Merge branch 'master' of http://47.109.37.87:3000/by2025/SmartParks
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run

This commit is contained in:
dy
2025-07-29 09:38:01 +08:00
26 changed files with 1801 additions and 19 deletions

View File

@@ -40,7 +40,7 @@ public class InspectionTaskDetailController extends BaseController {
/** /**
* 查询巡检明细列表 * 查询巡检明细列表
*/ */
@SaCheckPermission("system:taskDetail:list") //@SaCheckPermission("system:taskDetail:list")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo<InspectionTaskDetailVo> list(InspectionTaskDetailBo bo, PageQuery pageQuery) { public TableDataInfo<InspectionTaskDetailVo> list(InspectionTaskDetailBo bo, PageQuery pageQuery) {
return inspectionTaskDetailService.queryPageList(bo, pageQuery); return inspectionTaskDetailService.queryPageList(bo, pageQuery);

View File

@@ -1,11 +1,17 @@
package org.dromara.property.controller; package org.dromara.property.controller;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission; 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.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit; 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.domain.bo.ResidentPersonBo;
import org.dromara.property.service.IResidentPersonService; import org.dromara.property.service.IResidentPersonService;
import org.dromara.common.mybatis.core.page.TableDataInfo; 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 IResidentPersonService residentPersonService;
private final UploadFaceUtil uploadFaceUtil;
/** /**
* 查询入驻员工列表 * 查询入驻员工列表
*/ */
@@ -103,4 +112,39 @@ public class ResidentPersonController extends BaseController {
@PathVariable("ids") Long[] ids) { @PathVariable("ids") Long[] ids) {
return toAjax(residentPersonService.deleteWithValidByIds(List.of(ids), true)); 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

@@ -41,7 +41,7 @@ public class ResidentPersonBo extends BaseEntity {
/** /**
* 人员类型 * 人员类型
*/ */
@NotBlank(message = "人员类型不能为空", groups = { AddGroup.class, EditGroup.class }) //@NotBlank(message = "人员类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String type; private String type;
/** /**
* 性别 * 性别

View File

@@ -75,13 +75,13 @@ public class TbBuildingBo extends BaseEntity {
/** /**
* 经度 * 经度
*/ */
@NotBlank(message = "经度不能为空", groups = {AddGroup.class, EditGroup.class}) //@NotBlank(message = "经度不能为空", groups = {AddGroup.class, EditGroup.class})
private String lon; private String lon;
/** /**
* 维度 * 维度
*/ */
@NotBlank(message = "维度不能为空", groups = {AddGroup.class, EditGroup.class}) //@NotBlank(message = "维度不能为空", groups = {AddGroup.class, EditGroup.class})
private String lat; private String lat;
/** /**

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

@@ -5,6 +5,7 @@ import org.dromara.sis.sdk.hik.service.SdkBaseServer;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
* 物业模块 * 物业模块
@@ -12,6 +13,7 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
* @author ruoyi * @author ruoyi
*/ */
@EnableDubbo @EnableDubbo
@EnableScheduling
@SpringBootApplication @SpringBootApplication
public class SisApplication { public class SisApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,21 @@
//package org.dromara.sis.config;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.web.client.RestTemplate;
//
///**
// * 应用配置类
// */
//@Configuration
//public class SOSAppConfig {
//
// /**
// * 配置 RestTemplate 用于 API 调用
// * @return RestTemplate 实例
// */
// @Bean
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
//}

View File

@@ -0,0 +1,201 @@
package org.dromara.sis.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 报警记录实体类,对应数据库表 alarm_record
* 存储系统中的报警事件信息,包括设备信息、时间信息、处理状态等
*/
@Data
@TableName("alarm_record")
public class AlarmRecord implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 报警记录唯一标识,对应 API 返回的 Id 字段
* 采用 INPUT 策略,使用 API 返回的实际 ID 值
*/
@TableId(type = IdType.INPUT)
private Long id;
/**
* 任务编码,用于标识特定的报警任务
*/
private String taskCode;
/**
* 设备ID关联具体的报警设备
*/
private Integer deviceId;
/**
* 设备名称,如 "7楼办公室"
*/
private String deviceName;
/**
* 设备许可证ID用于唯一标识设备
*/
private String deviceLicenseId;
/**
* 设备SIP号码用于通信
*/
private String deviceSipNum;
/**
* 设备所在经度
*/
private Double deviceLng;
/**
* 设备所在纬度
*/
private Double deviceLat;
/**
* 会议ID关联报警处理过程中的会议
*/
private Long conferenceId;
/**
* 会议SIP编码
*/
private String confSipCode;
/**
* 报警状态,如 "finished"(已完成)、"noAnswer"(未接听)等
*/
private String state;
/**
* 报警开始时间Java Date 类型)
*/
private Date startTime;
/**
* 报警开始时间的 Unix 时间戳(毫秒)
*/
private Long startTimeUnix;
/**
* 报警结束时间Java Date 类型)
*/
private Date finishTime;
/**
* 报警结束时间的 Unix 时间戳(毫秒)
*/
private Long finishTimeUnix;
/**
* 响铃开始时间Java Date 类型)
*/
private Date ringingTime;
/**
* 响铃开始时间的 Unix 时间戳(毫秒)
*/
private Long ringingTimeUnix;
/**
* 过期时间Java Date 类型)
*/
private Date expireTime;
/**
* 过期时间的 Unix 时间戳(毫秒)
*/
private Long expireTimeUnix;
/**
* 呼叫过期时间Java Date 类型)
*/
private Date callExpireTime;
/**
* 呼叫过期时间的 Unix 时间戳(毫秒)
*/
private Long callExpireTimeUnix;
/**
* 呼叫开始时间Java Date 类型)
*/
private Date callTime;
/**
* 呼叫开始时间的 Unix 时间戳(毫秒)
*/
private Long callTimeUnix;
/**
* 设备是否带有摄像头0-不带1-带)
*/
private Integer deviceWithCamera;
/**
* 公司编码,标识所属公司
*/
private String companyCode;
/**
* 报警类型,如 "button"(按钮报警)
*/
private String alarmType;
/**
* 业务类型,如 "normal"(正常业务)
*/
private String businessType;
/**
* 分组ID用于对设备进行分组管理
*/
private Integer groupId;
/**
* 报告通知级别
*/
private Integer reportNotifyLevel;
/**
* 是否挂起0-未挂起1-挂起)
*/
private Integer isHold;
/**
* 显示的报警类型(可能为空)
*/
private String displayAlarmType;
/**
* 接收类型(可能为空)
*/
private String acceptType;
/**
* 分组名称(可能为空)
*/
private String groupName;
/**
* 设备联系人(可能为空)
*/
private String deviceLinkman;
/**
* 设备联系电话(可能为空)
*/
private String devicePhoneNum;
/**
* 记录创建时间,由数据库自动填充
* 使用 MyBatis-Plus 的自动填充功能,插入时自动设置为当前时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,99 @@
package org.dromara.sis.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 报警任务操作记录实体类,对应数据库表 alarm_record_task_operator
* 记录每个报警任务的操作人信息和处理时间
*/
@Data
@TableName("alarm_record_task_operator")
public class AlarmTaskOperator implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 自增主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 关联的报警记录ID外键
*/
private Long alarmRecordId;
/**
* 操作记录ID来自API返回
*/
private Integer operatorId;
/**
* 关联的报警ID
*/
private Long alarmId;
/**
* 任务编码
*/
private String taskCode;
/**
* 操作人ID
*/
private Integer userId;
/**
* 操作人类型1-普通用户2-管理员等,具体根据业务定义)
*/
private Integer userType;
/**
* 操作人昵称
*/
private String nickName;
/**
* 操作人头像URL
*/
private String avatarUrl;
/**
* 操作人SIP号码
*/
private String sipNum;
/**
* 接听时间Java Date 类型)
*/
private Date answerTime;
/**
* 接听时间的 Unix 时间戳(毫秒)
*/
private Long answerTimeUnix;
/**
* 处理完成时间Java Date 类型)
*/
private Date finishTime;
/**
* 处理完成时间的 Unix 时间戳(毫秒)
*/
private Long finishTimeUnix;
/**
* 转接级别(如 "levelOne"、"levelTwo" 等)
*/
private String transferLevel;
/**
* 记录创建时间,由数据库自动填充
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,14 @@
package org.dromara.sis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.sis.domain.AlarmRecord;
import org.springframework.stereotype.Repository;
/**
* 报警记录 Mapper 接口
*/
@Repository
public interface AlarmRecordMapper extends BaseMapper<AlarmRecord> {
}

View File

@@ -0,0 +1,13 @@
package org.dromara.sis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.sis.domain.AlarmTaskOperator;
import org.springframework.stereotype.Repository;
/**
* 报警任务操作记录 Mapper 接口
*/
@Repository
public interface AlarmTaskOperatorMapper extends BaseMapper<AlarmTaskOperator> {
}

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
package org.dromara.sis.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.sis.domain.AlarmRecord;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* 报警记录服务接口
*/
public interface AlarmRecordService extends IService<AlarmRecord> {
/**
* 从 API 返回的 Map 数据转换为实体对象
* @param map API 返回的单个报警记录数据
* @return 转换后的实体对象
*/
AlarmRecord convertFromMap(Map<String, Object> map);
@Transactional(rollbackFor = Exception.class)
int saveOrUpdateRecords(List<AlarmRecord> records, Map<Long, List<Map<String, Object>>> operatorMap);
/**
* 保存新的报警记录,并关联保存其操作记录
* @param records 报警记录列表
* @param operatorMap 操作记录映射键为报警记录ID值为对应操作记录列表
* @return 成功保存的记录数
*/
int saveNewRecords(List<AlarmRecord> records, Map<Long, List<Map<String, Object>>> operatorMap);
}

View File

@@ -0,0 +1,34 @@
package org.dromara.sis.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.sis.domain.AlarmTaskOperator;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* 报警任务操作记录服务接口
*/
public interface AlarmTaskOperatorService extends IService<AlarmTaskOperator> {
/**
* 从 API 返回的 Map 数据转换为实体列表
* @param alarmRecordId 关联的报警记录ID
* @param operatorMaps API 返回的操作人数据列表
* @return 转换后的实体列表
*/
List<AlarmTaskOperator> convertFromMaps(Long alarmRecordId, List<Map<String, Object>> operatorMaps);
@Transactional(rollbackFor = Exception.class)
int saveOrUpdateOperators(List<AlarmTaskOperator> operators);
/**
* 批量保存操作记录,自动过滤已存在的记录
* @param operators 操作记录列表
* @return 成功保存的记录数
*/
int saveNewOperators(List<AlarmTaskOperator> operators);
}

View File

@@ -0,0 +1,18 @@
package org.dromara.sis.service;
import java.util.Map;
/**
* API服务接口定义与外部API通信的方法
*/
public interface ApiService {
/**
* 获取报警记录列表
* @param pageNum 页码
* @param pageSize 每页数量
* @return API返回的结果数据包含状态码和记录列表
*/
Map<String, Object> fetchAlarmRecords(int pageNum, int pageSize);
}

View File

@@ -0,0 +1,265 @@
package org.dromara.sis.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sis.domain.AlarmRecord;
import org.dromara.sis.domain.AlarmTaskOperator;
import org.dromara.sis.mapper.AlarmRecordMapper;
import org.dromara.sis.service.AlarmRecordService;
import org.dromara.sis.service.AlarmTaskOperatorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* 报警记录服务实现类
*/
@Slf4j
@Service
public class AlarmRecordServiceImpl extends ServiceImpl<AlarmRecordMapper, AlarmRecord> implements AlarmRecordService {
@Autowired
private AlarmTaskOperatorService taskOperatorService;
@Transactional(rollbackFor = Exception.class)
@Override
public int saveOrUpdateRecords(List<AlarmRecord> records, Map<Long, List<Map<String, Object>>> operatorMap) {
if (records == null || records.isEmpty()) return 0;
// 提取记录ID列表
List<Long> ids = records.stream().map(AlarmRecord::getId).collect(Collectors.toList());
// 查询数据库中已存在的记录
LambdaQueryWrapper<AlarmRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(AlarmRecord::getId, ids);
List<AlarmRecord> existingRecords = this.list(queryWrapper);
// 创建现有记录的映射,便于快速查找
Map<Long, AlarmRecord> existingRecordMap = existingRecords.stream()
.collect(Collectors.toMap(AlarmRecord::getId, record -> record));
List<AlarmRecord> newRecords = new ArrayList<>();
List<AlarmRecord> updateRecords = new ArrayList<>();
// 分类处理记录
for (AlarmRecord record : records) {
if (existingRecordMap.containsKey(record.getId())) {
// 如果记录已存在,检查是否需要更新
AlarmRecord existing = existingRecordMap.get(record.getId());
if (hasRecordChanges(existing, record)) {
updateRecords.add(record);
}
} else {
// 新记录
newRecords.add(record);
}
}
int result = 0;
// 批量插入新记录
if (!newRecords.isEmpty()) {
if (this.saveBatch(newRecords)) {
result += newRecords.size();
// 保存关联的操作记录
saveRelatedOperators(newRecords, operatorMap);
}
}
// 批量更新修改过的记录
if (!updateRecords.isEmpty()) {
if (this.updateBatchById(updateRecords)) {
result += updateRecords.size();
// 保存关联的操作记录
saveRelatedOperators(updateRecords, operatorMap);
}
}
return result;
}
/**
* 检查两个AlarmRecord对象是否有变化
*/
private boolean hasRecordChanges(AlarmRecord existing, AlarmRecord updated) {
return !java.util.Objects.equals(existing.getTaskCode(), updated.getTaskCode()) ||
!java.util.Objects.equals(existing.getDeviceId(), updated.getDeviceId()) ||
!java.util.Objects.equals(existing.getDeviceName(), updated.getDeviceName()) ||
!java.util.Objects.equals(existing.getDeviceLicenseId(), updated.getDeviceLicenseId()) ||
!java.util.Objects.equals(existing.getDeviceSipNum(), updated.getDeviceSipNum()) ||
!java.util.Objects.equals(existing.getDeviceLng(), updated.getDeviceLng()) ||
!java.util.Objects.equals(existing.getDeviceLat(), updated.getDeviceLat()) ||
!java.util.Objects.equals(existing.getConferenceId(), updated.getConferenceId()) ||
!java.util.Objects.equals(existing.getConfSipCode(), updated.getConfSipCode()) ||
!java.util.Objects.equals(existing.getState(), updated.getState()) ||
!java.util.Objects.equals(existing.getStartTime(), updated.getStartTime()) ||
!java.util.Objects.equals(existing.getStartTimeUnix(), updated.getStartTimeUnix()) ||
!java.util.Objects.equals(existing.getFinishTime(), updated.getFinishTime()) ||
!java.util.Objects.equals(existing.getFinishTimeUnix(), updated.getFinishTimeUnix()) ||
!java.util.Objects.equals(existing.getRingingTime(), updated.getRingingTime()) ||
!java.util.Objects.equals(existing.getRingingTimeUnix(), updated.getRingingTimeUnix()) ||
!java.util.Objects.equals(existing.getExpireTime(), updated.getExpireTime()) ||
!java.util.Objects.equals(existing.getExpireTimeUnix(), updated.getExpireTimeUnix()) ||
!java.util.Objects.equals(existing.getCallExpireTime(), updated.getCallExpireTime()) ||
!java.util.Objects.equals(existing.getCallExpireTimeUnix(), updated.getCallExpireTimeUnix()) ||
!java.util.Objects.equals(existing.getCallTime(), updated.getCallTime()) ||
!java.util.Objects.equals(existing.getCallTimeUnix(), updated.getCallTimeUnix()) ||
!java.util.Objects.equals(existing.getDeviceWithCamera(), updated.getDeviceWithCamera()) ||
!java.util.Objects.equals(existing.getCompanyCode(), updated.getCompanyCode()) ||
!java.util.Objects.equals(existing.getAlarmType(), updated.getAlarmType()) ||
!java.util.Objects.equals(existing.getBusinessType(), updated.getBusinessType()) ||
!java.util.Objects.equals(existing.getGroupId(), updated.getGroupId()) ||
!java.util.Objects.equals(existing.getReportNotifyLevel(), updated.getReportNotifyLevel()) ||
!java.util.Objects.equals(existing.getIsHold(), updated.getIsHold()) ||
!java.util.Objects.equals(existing.getDisplayAlarmType(), updated.getDisplayAlarmType()) ||
!java.util.Objects.equals(existing.getAcceptType(), updated.getAcceptType()) ||
!java.util.Objects.equals(existing.getGroupName(), updated.getGroupName()) ||
!java.util.Objects.equals(existing.getDeviceLinkman(), updated.getDeviceLinkman()) ||
!java.util.Objects.equals(existing.getDevicePhoneNum(), updated.getDevicePhoneNum());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int saveNewRecords(List<AlarmRecord> records, Map<Long, List<Map<String, Object>>> operatorMap) {
if (records == null || records.isEmpty()) return 0;
// 提取待插入记录的ID列表
List<Long> ids = records.stream().map(AlarmRecord::getId).collect(Collectors.toList());
// 查询数据库中已存在的记录
LambdaQueryWrapper<AlarmRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(AlarmRecord::getId, ids);
List<AlarmRecord> existingRecords = this.list(queryWrapper);
List<Long> existingIds = existingRecords.stream().map(AlarmRecord::getId).collect(Collectors.toList());
// 过滤出新增记录
List<AlarmRecord> newRecords = records.stream()
.filter(record -> !existingIds.contains(record.getId()))
.collect(Collectors.toList());
// 批量插入新记录
int savedCount = 0;
if (!newRecords.isEmpty()) {
if (this.saveBatch(newRecords)) {
savedCount = newRecords.size();
// 保存关联的操作记录
saveRelatedOperators(newRecords, operatorMap);
}
}
return savedCount;
}
private void saveRelatedOperators(List<AlarmRecord> newRecords, Map<Long, List<Map<String, Object>>> operatorMap) {
if (operatorMap == null || operatorMap.isEmpty()) return;
// 收集需要保存的操作记录
List<AlarmTaskOperator> allOperators = new ArrayList<>();
for (AlarmRecord record : newRecords) {
Long recordId = record.getId();
if (operatorMap.containsKey(recordId)) {
List<Map<String, Object>> operatorsData = operatorMap.get(recordId);
List<AlarmTaskOperator> operators = taskOperatorService.convertFromMaps(recordId, operatorsData);
allOperators.addAll(operators);
}
}
// 批量保存操作记录
if (!allOperators.isEmpty()) {
taskOperatorService.saveOrUpdateOperators(allOperators);
}
}
@Override
public AlarmRecord convertFromMap(Map<String, Object> map) {
if (map == null) return null;
AlarmRecord record = new AlarmRecord();
record.setId(getLongValue(map, "Id"));
record.setTaskCode((String) map.get("TaskCode"));
record.setDeviceId(getIntValue(map, "DeviceId"));
record.setDeviceName((String) map.get("DeviceName"));
record.setDeviceLicenseId((String) map.get("DeviceLicenseId"));
record.setDeviceSipNum((String) map.get("DeviceSipNum"));
record.setDeviceLng(getDoubleValue(map, "DeviceLng"));
record.setDeviceLat(getDoubleValue(map, "DeviceLat"));
record.setConferenceId(getLongValue(map, "ConferenceId"));
record.setConfSipCode((String) map.get("ConfSipCode"));
record.setState((String) map.get("State"));
// 处理时间字段
record.setStartTime(parseDate(map, "StartTime"));
record.setStartTimeUnix(getLongValue(map, "StartTimeUnix"));
record.setFinishTime(parseDate(map, "FinishTime"));
record.setFinishTimeUnix(getLongValue(map, "FinishTimeUnix"));
record.setRingingTime(parseDate(map, "RingingTime"));
record.setRingingTimeUnix(getLongValue(map, "RingingTimeUnix"));
record.setExpireTime(parseDate(map, "ExpireTime"));
record.setExpireTimeUnix(getLongValue(map, "ExpireTimeUnix"));
record.setCallExpireTime(parseDate(map, "CallExpireTime"));
record.setCallExpireTimeUnix(getLongValue(map, "CallExpireTimeUnix"));
record.setCallTime(parseDate(map, "CallTime"));
record.setCallTimeUnix(getLongValue(map, "CallTimeUnix"));
record.setDeviceWithCamera(getIntValue(map, "DeviceWithCamera"));
record.setCompanyCode((String) map.get("CompanyCode"));
record.setAlarmType((String) map.get("AlarmType"));
record.setBusinessType((String) map.get("BusinessType"));
record.setGroupId(getIntValue(map, "GroupId"));
record.setReportNotifyLevel(getIntValue(map, "ReportNotifyLevel"));
record.setIsHold(getIntValue(map, "IsHold"));
record.setDisplayAlarmType((String) map.get("DisplayAlarmType"));
record.setAcceptType((String) map.get("AcceptType"));
record.setGroupName((String) map.get("GroupName"));
record.setDeviceLinkman((String) map.get("DeviceLinkman"));
record.setDevicePhoneNum((String) map.get("DevicePhoneNum"));
return record;
}
// 类型转换辅助方法
private Long getLongValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
}
return null;
}
private Integer getIntValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
return null;
}
private Double getDoubleValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return null;
}
private java.util.Date parseDate(Map<String, Object> map, String key) {
try {
String dateStr = (String) map.get(key);
if (dateStr != null && !dateStr.isEmpty() && !"1970-01-01 00:00:00".equals(dateStr)) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(dateStr);
}
} catch (Exception e) {
log.error("日期转换失败: {}", e.getMessage());
}
return null;
}
}

View File

@@ -0,0 +1,215 @@
package org.dromara.sis.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sis.domain.AlarmTaskOperator;
import org.dromara.sis.mapper.AlarmTaskOperatorMapper;
import org.dromara.sis.service.AlarmTaskOperatorService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 报警任务操作记录服务实现类
*/
@Slf4j
@Service
public class AlarmTaskOperatorServiceImpl extends ServiceImpl<AlarmTaskOperatorMapper, AlarmTaskOperator> implements AlarmTaskOperatorService {
@Transactional(rollbackFor = Exception.class)
@Override
public int saveOrUpdateOperators(List<AlarmTaskOperator> operators) {
if (operators == null || operators.isEmpty()) return 0;
// 生成唯一键集合 (alarmRecordId_operatorId)
List<String> uniqueKeys = operators.stream()
.map(op -> op.getAlarmRecordId() + "_" + op.getOperatorId())
.collect(Collectors.toList());
// 查询已存在的记录
LambdaQueryWrapper<AlarmTaskOperator> queryWrapper = new LambdaQueryWrapper<>();
boolean first = true;
for (String uniqueKey : uniqueKeys) {
String[] parts = uniqueKey.split("_");
if (parts.length == 2) {
Long alarmRecordId = Long.parseLong(parts[0]);
Integer operatorId = Integer.parseInt(parts[1]);
if (!first) {
queryWrapper.or();
}
queryWrapper.nested(wrapper ->
wrapper.eq(AlarmTaskOperator::getAlarmRecordId, alarmRecordId)
.eq(AlarmTaskOperator::getOperatorId, operatorId)
);
first = false;
}
}
List<AlarmTaskOperator> existingRecords = this.list(queryWrapper);
// 创建现有记录的映射,便于快速查找
Map<String, AlarmTaskOperator> existingRecordMap = existingRecords.stream()
.collect(Collectors.toMap(
op -> op.getAlarmRecordId() + "_" + op.getOperatorId(),
op -> op
));
List<AlarmTaskOperator> newRecords = new ArrayList<>();
List<AlarmTaskOperator> updateRecords = new ArrayList<>();
// 分类处理记录
for (AlarmTaskOperator operator : operators) {
String key = operator.getAlarmRecordId() + "_" + operator.getOperatorId();
if (existingRecordMap.containsKey(key)) {
// 如果记录已存在,检查是否需要更新
AlarmTaskOperator existing = existingRecordMap.get(key);
if (hasChanges(existing, operator)) {
// 更新ID以确保正确更新
operator.setId(existing.getId());
updateRecords.add(operator);
}
} else {
// 新记录
newRecords.add(operator);
}
}
int result = 0;
// 批量插入新记录
if (!newRecords.isEmpty()) {
result += this.saveBatch(newRecords) ? newRecords.size() : 0;
}
// 批量更新修改过的记录
if (!updateRecords.isEmpty()) {
result += this.updateBatchById(updateRecords) ? updateRecords.size() : 0;
}
return result;
}
/**
* 检查两个AlarmTaskOperator对象是否有变化
*/
private boolean hasChanges(AlarmTaskOperator existing, AlarmTaskOperator updated) {
return !java.util.Objects.equals(existing.getAlarmId(), updated.getAlarmId()) ||
!java.util.Objects.equals(existing.getTaskCode(), updated.getTaskCode()) ||
!java.util.Objects.equals(existing.getUserId(), updated.getUserId()) ||
!java.util.Objects.equals(existing.getUserType(), updated.getUserType()) ||
!java.util.Objects.equals(existing.getNickName(), updated.getNickName()) ||
!java.util.Objects.equals(existing.getAvatarUrl(), updated.getAvatarUrl()) ||
!java.util.Objects.equals(existing.getSipNum(), updated.getSipNum()) ||
!java.util.Objects.equals(existing.getAnswerTime(), updated.getAnswerTime()) ||
!java.util.Objects.equals(existing.getAnswerTimeUnix(), updated.getAnswerTimeUnix()) ||
!java.util.Objects.equals(existing.getFinishTime(), updated.getFinishTime()) ||
!java.util.Objects.equals(existing.getFinishTimeUnix(), updated.getFinishTimeUnix()) ||
!java.util.Objects.equals(existing.getTransferLevel(), updated.getTransferLevel());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int saveNewOperators(List<AlarmTaskOperator> operators) {
if (operators == null || operators.isEmpty()) return 0;
// 生成唯一键集合 (alarmRecordId_operatorId)
List<String> uniqueKeys = operators.stream()
.map(op -> op.getAlarmRecordId() + "_" + op.getOperatorId())
.collect(Collectors.toList());
// 查询已存在的记录(修正后的代码)
LambdaQueryWrapper<AlarmTaskOperator> queryWrapper = new LambdaQueryWrapper<>();
for (String uniqueKey : uniqueKeys) {
String[] parts = uniqueKey.split("_");
if (parts.length == 2) {
Long alarmRecordId = Long.parseLong(parts[0]);
Integer operatorId = Integer.parseInt(parts[1]);
if (!queryWrapper.isEmptyOfWhere()) {
queryWrapper.or();
}
queryWrapper.nested(wrapper ->
wrapper.eq(AlarmTaskOperator::getAlarmRecordId, alarmRecordId)
.eq(AlarmTaskOperator::getOperatorId, operatorId)
);
}
}
List<AlarmTaskOperator> existingRecords = this.list(queryWrapper);
// 生成已存在的唯一键集合
List<String> existingKeys = existingRecords.stream()
.map(op -> op.getAlarmRecordId() + "_" + op.getOperatorId())
.collect(Collectors.toList());
// 过滤出新记录
List<AlarmTaskOperator> newRecords = operators.stream()
.filter(op -> !existingKeys.contains(op.getAlarmRecordId() + "_" + op.getOperatorId()))
.collect(Collectors.toList());
// 批量插入新记录
return newRecords.isEmpty() ? 0 : this.saveBatch(newRecords) ? newRecords.size() : 0;
}
@Override
public List<AlarmTaskOperator> convertFromMaps(Long alarmRecordId, List<Map<String, Object>> operatorMaps) {
List<AlarmTaskOperator> result = new ArrayList<>();
if (operatorMaps == null || operatorMaps.isEmpty()) return result;
for (Map<String, Object> map : operatorMaps) {
AlarmTaskOperator operator = new AlarmTaskOperator();
operator.setAlarmRecordId(alarmRecordId);
operator.setOperatorId(getIntValue(map, "Id"));
operator.setAlarmId(getLongValue(map, "AlarmId"));
operator.setTaskCode((String) map.get("TaskCode"));
operator.setUserId(getIntValue(map, "UserId"));
operator.setUserType(getIntValue(map, "UserType"));
operator.setNickName((String) map.get("NickName"));
operator.setAvatarUrl((String) map.get("AvatarUrl"));
operator.setSipNum((String) map.get("SipNum"));
operator.setAnswerTime(parseDate(map, "AnswerTime"));
operator.setAnswerTimeUnix(getLongValue(map, "AnswerTimeUnix"));
operator.setFinishTime(parseDate(map, "FinishTime"));
operator.setFinishTimeUnix(getLongValue(map, "FinishTimeUnix"));
operator.setTransferLevel((String) map.get("TransferLevel"));
result.add(operator);
}
return result;
}
// 类型转换辅助方法
private Long getLongValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
}
return null;
}
private Integer getIntValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
return null;
}
private java.util.Date parseDate(Map<String, Object> map, String key) {
try {
String dateStr = (String) map.get(key);
if (dateStr != null && !dateStr.isEmpty() && !"1970-01-01 00:00:00".equals(dateStr)) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(dateStr);
}
} catch (Exception e) {
log.error("日期转换失败: {}", e.getMessage());
}
return null;
}
}

View File

@@ -0,0 +1,291 @@
package org.dromara.sis.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sis.service.ApiService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* API服务实现类支持获取Token、刷新Token和401响应处理
*/
@Service
@Slf4j
public class ApiServiceImpl implements ApiService {
@Value("${api.url}")
private String apiUrl;
@Value("${api.authUrl}")
private String authUrl; // 认证URL: /sos/v1/mntn/account/appId/token
@Value("${api.refreshUrl}")
private String refreshUrl; // 刷新Token URL: /sos/v1/mntn/account/refresh/token
@Value("${api.appId}")
private String appId;
@Value("${api.appCode}")
private String appCode;
private final RestTemplate restTemplate;
// 缓存Token信息
private static final Map<String, TokenInfo> tokenCache = new ConcurrentHashMap<>();
public ApiServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
@Retryable(value = {HttpClientErrorException.Unauthorized.class}, maxAttempts = 2, backoff = @Backoff(delay = 1000))
public Map<String, Object> fetchAlarmRecords(int pageNum, int pageSize) {
String token = getAccessToken();
// 设置请求头添加Token
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("token", token); // 根据接口要求,使用"Token"而非"Authorization"
// 构建请求体
Map<String, Object> requestBody = buildRequestParams(pageNum, pageSize);
log.info("请求参数:{}", requestBody);
try {
// 发送带Token的请求
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.exchange(
apiUrl,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("请求返回:{}", response);
if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> result = response.getBody();
if (result != null && result.get("Status").equals(0)) {
return result;
} else {
log.error("API返回错误状态: {}", result);
return null;
}
} else {
log.error("API请求失败状态码: {}", response.getStatusCode());
return null;
}
} catch (HttpClientErrorException.Unauthorized e) {
// 捕获401错误清除缓存的Token并重试
log.warn("Token已过期清除缓存并尝试重新获取: {}", e.getMessage());
tokenCache.remove(appId);
// 重新获取Token并再次调用由@Retryable注解处理
throw e;
} catch (Exception e) {
log.error("调用API异常: {}", e.getMessage(), e);
return null;
}
}
/**
* 获取访问Token
*/
private synchronized String getAccessToken() {
// 检查缓存中是否有有效的Token
TokenInfo tokenInfo = tokenCache.get(appId);
if (tokenInfo != null && !isTokenExpired(tokenInfo)) {
return tokenInfo.getAccessToken();
}
// 尝试刷新Token
if (tokenInfo != null && tokenInfo.getRefreshToken() != null) {
log.info("尝试刷新现有Token...");
try {
String refreshedToken = refreshAccessToken(tokenInfo.getRefreshToken());
if (refreshedToken != null) {
return refreshedToken;
}
} catch (Exception e) {
log.warn("刷新Token失败将重新获取: {}", e.getMessage());
}
}
// 缓存中没有有效Token或刷新失败重新获取
log.info("正在获取新的访问Token...");
try {
// 构建获取Token的请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("AppId", appId);
body.put("AppCode", appCode);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
// 发送获取Token的请求
ResponseEntity<Map> response = restTemplate.postForEntity(
authUrl,
request,
Map.class
);
log.info("接口响应:{},状态码:{}",response,response.getStatusCode());
if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> tokenResponse = response.getBody();
if (tokenResponse != null && tokenResponse.get("Status").equals(0)) {
String accessToken = (String) tokenResponse.get("Token");
String refreshToken = (String) tokenResponse.get("RefreshToken");
Integer expireIn = (Integer) tokenResponse.get("ExpireIn");
// 缓存新获取的Token设置过期时间提前1分钟避免临界点问题
TokenInfo newTokenInfo = new TokenInfo(
accessToken,
refreshToken,
System.currentTimeMillis() + (expireIn - 60) * 1000
);
tokenCache.put(appId, newTokenInfo);
log.info("成功获取新的访问Token有效期: {}秒", expireIn);
return accessToken;
}
}
log.error("获取Token失败状态码: {}", response.getStatusCode());
throw new RuntimeException("获取访问Token失败");
} catch (Exception e) {
log.error("获取Token异常: {}", e.getMessage(), e);
throw new RuntimeException("获取访问Token异常", e);
}
}
/**
* 刷新访问Token
*/
private String refreshAccessToken(String refreshToken) {
try {
// 构建刷新Token的请求体
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("RefreshToken", refreshToken);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
// 发送刷新Token的请求
ResponseEntity<Map> response = restTemplate.postForEntity(
refreshUrl,
request,
Map.class
);
if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> tokenResponse = response.getBody();
if (tokenResponse != null && tokenResponse.get("Status").equals(0)) {
String accessToken = (String) tokenResponse.get("Token");
String newRefreshToken = (String) tokenResponse.get("RefreshToken");
Integer expireIn = (Integer) tokenResponse.get("ExpireIn");
// 更新缓存的Token信息
TokenInfo tokenInfo = tokenCache.get(appId);
if (tokenInfo != null) {
tokenInfo.setAccessToken(accessToken);
tokenInfo.setRefreshToken(newRefreshToken);
tokenInfo.setExpireTime(System.currentTimeMillis() + (expireIn - 60) * 1000);
}
log.info("成功刷新访问Token有效期: {}秒", expireIn);
return accessToken;
}
}
log.warn("刷新Token失败将重新获取");
return null;
} catch (Exception e) {
log.warn("刷新Token异常: {}", e.getMessage());
return null;
}
}
/**
* 检查Token是否已过期
*/
private boolean isTokenExpired(TokenInfo tokenInfo) {
return tokenInfo.getExpireTime() < System.currentTimeMillis();
}
/**
* 构建请求参数
*/
private Map<String, Object> buildRequestParams(int pageNum, int pageSize) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("PageSize", pageSize);
requestBody.put("PageNum", pageNum);
requestBody.put("OrFirst", true);
requestBody.put("Filters", Collections.emptyList()); // 空过滤器
requestBody.put("OrderKey", "StartTime");
requestBody.put("Desc", 1); // 降序排列
return requestBody;
}
// private Map<String, Object> buildRequestParams(int pageNum, int pageSize) {
// Map<String, Object> requestBody = new HashMap<>();
// requestBody.put("'PageSize'", pageSize);
// requestBody.put("'PageNum'", pageNum);
//
// List<List<Map<String, Object>>> filters = new ArrayList<>();
// requestBody.put("'Filters'", filters);
//
// requestBody.put("'OrderKey'", "'StartTime'");
// requestBody.put("'Desc'", "1");
// requestBody.put("'OrFirst'", true);
//
// return requestBody;
// }
/**
* Token信息内部类修改为可更新的类
*/
private static class TokenInfo {
private String accessToken;
private String refreshToken;
private long expireTime;
public TokenInfo(String accessToken, String refreshToken, long expireTime) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expireTime = expireTime;
}
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public long getExpireTime() {
return expireTime;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
}
}

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.context.mock.SaTokenContextMockUtil;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
@@ -35,14 +35,14 @@ import java.util.concurrent.atomic.AtomicReference;
/** /**
* @author lsm * @author lsm
* @apiNote AuthTimer * @apiNote AuthSyncTask
* @since 2025/7/26 * @since 2025/7/26
*/ */
@Slf4j @Slf4j
@Configuration @Configuration
@EnableScheduling @EnableScheduling
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthTimer { public class AuthSyncTask {
@DubboReference @DubboReference
private RemoteFileService remoteFileService; private RemoteFileService remoteFileService;

View File

@@ -0,0 +1,101 @@
package org.dromara.sis.task;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.dromara.property.api.RemoteFloorService;
import org.dromara.property.api.domain.vo.RemoteFloorVo;
import org.dromara.sis.domain.AlarmRecord;
import org.dromara.sis.service.AlarmRecordService;
import org.dromara.sis.service.ApiService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
/**
* 数据同步定时任务
* 每分钟执行一次,从 API 获取最新报警记录并同步到数据库
*/
@Slf4j
@Component
public class DataSyncTask {
private final ApiService apiService;
private final AlarmRecordService alarmRecordService;
@Value("${sync.pageSize:100}")
private int pageSize;
public DataSyncTask(ApiService apiService, AlarmRecordService alarmRecordService) {
this.apiService = apiService;
this.alarmRecordService = alarmRecordService;
}
/**
* 定时同步报警记录数据
*/
@Scheduled(fixedRate = 60 * 1000) // 每分钟执行一次
public void syncAlarmRecords() {
log.info("开始同步报警记录数据...");
int pageNum = 1;
boolean hasMoreData = true;
int totalSynced = 0;
try {
while (hasMoreData) {
// 调用 API 获取数据
Map<String, Object> apiResult = apiService.fetchAlarmRecords(pageNum, pageSize);
if (apiResult == null) {
log.error("API 返回空结果,停止同步");
break;
}
// 解析数据
int totalNum = (int) apiResult.getOrDefault("TotalNum", 0);
List<Map<String, Object>> recordList = (List<Map<String, Object>>) apiResult.get("RecordList");
if (recordList == null || recordList.isEmpty()) {
log.info("没有更多数据可同步");
hasMoreData = false;
break;
}
// 转换报警记录
List<AlarmRecord> records = recordList.stream()
.map(alarmRecordService::convertFromMap)
.collect(Collectors.toList());
// 提取操作记录数据
Map<Long, List<Map<String, Object>>> operatorMap = new HashMap<>();
for (Map<String, Object> recordMap : recordList) {
Long recordId = Long.valueOf((Integer)recordMap.get("Id"));
List<Map<String, Object>> operators = (List<Map<String, Object>>) recordMap.get("TaskOperators");
if (operators != null && !operators.isEmpty()) {
operatorMap.put(recordId, operators);
}
}
// 保存数据(包含操作记录)
int savedCount = alarmRecordService.saveOrUpdateRecords(records, operatorMap);
totalSynced += savedCount;
log.info("第 {} 页同步完成,共 {} 条记录,新增 {} 条", pageNum, recordList.size(), savedCount);
// 判断是否还有更多数据
if (pageNum * pageSize >= totalNum) {
hasMoreData = false;
} else {
pageNum++;
}
}
log.info("报警记录数据同步完成,共新增 {} 条记录", totalSynced);
} catch (Exception e) {
log.error("同步报警记录数据失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -10,6 +10,16 @@ spring:
profiles: profiles:
# 环境配置 # 环境配置
active: @profiles.active@ active: @profiles.active@
# API 配置
api:
url: https://norsos.lionking110.com/sos/v1/mntn/business/appId/alarm/list
authUrl: https://norsos.lionking110.com/sos/v1/mntn/account/appId/token
refreshUrl: https://norsos.lionking110.com/sos/v1/mntn/account/refresh/token
appId: dfc7ec7507de4626b8c920c4fe1ff8b1
appCode: fe11d05aa5704dffaa0b1c4b56ba80b2
# 同步配置
sync:
pageSize: 100
--- # nacos 配置 --- # nacos 配置
spring: spring:

View File

@@ -40,9 +40,9 @@ spring.sql.init.platform=mysql
db.num=1 db.num=1
### Connect URL of DB: ### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true db.url.0=jdbc:mysql://47.109.37.87:23306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
db.user.0=root db.user.0=root
db.password.0=root db.password.0=admin@123456
### the maximum retry times for push ### the maximum retry times for push
nacos.config.push.maxRetryTime=50 nacos.config.push.maxRetryTime=50