Offline数据同步解决方案同步规则和SDF的用法

本文详细介绍了使用SQL Server Compact Edition 3.5(SSCE)进行数据同步的过程,包括同步规则、SSCE连接字符串、辅助类以及SyncSDF文件的结构和用途。特别讨论了如何查询和应用服务端的更改,以及同步过程中的事务管理和时间戳列[__sysChangeTxBsn][__sysInsertTxBsn]的作用。

1.  说明:本项目中使用的是SSCE3.5版本。

2.  同步规则:

只下载的数据,采用SnopShot模式下载;双向同步和只上传同步都采用Bidirectional模式;同步时间频率如下:

只下载:轮询同步程序,计划同步1d/次;同时,程序中做可以触发的同步;

双向同步:轮询同步程序,计划同步5mi/次;定时清理:轮询同步程序,计划同步1d/次;

只上传:同步轮询程序,计划同步1d/次;定时清理:同步轮询程序,计划同步1M/次;

当主程序登录成功后,启动后台同步线程,5分钟执行一次同步,同步时校验当前时间与配置文件中保存的LastSyncDateTime的时间差是否大于执行频率来判断是否本次执行同步。后台线程在主程序完全退出后自动结束。

 

3.  SSCE访问字符串的书写总结:

举例:connectionString="Data Source=..\Database\DIH_AMS.sdf;PersistSecurity Info = False;Max Database Size = 4000; Max Buffer Size = 1024;"

项目中常用的连接SDF的写法。解释如下:

Data Source:SDF的路径和名称,上面用到了相对路径;

Persist Security Info:是否保存密码,这里true/false影响不大;

Max Database Size:SDF文件的最大支持容量,默认128,最大支持到4G,但只能写到<4091;

Max Buffer Size:缓存大小,默认640;

SDF文件,可以设置访问密码,不像MS-Server要设置UserID和Password,比如” Password= 'dih+dih123';”,设置密码时,需要Server Studio打开SDF,在属性里面添加密码。

自动压缩设置,比如”SSCE:AutoShrink Threshold=60;”这里不写的话默认也是60%,表示实际容量不超过容量的60%时,自动压缩;

参考:http://msdn.microsoft.com/zh-cn/library/system.data.sqlserverce.sqlceconnection.connectionstring(VS.85).aspx

4.  SSCE访问类Helper:

#region InsertLocalData
public static bool InsertLocalData(string strSql, string clientConnStr)
{
	bool b = true;
	using (SqlCeConnection conn = new SqlCeConnection(clientConnStr))
	{
		try
		{
			conn.Open();
			SqlCeCommand cmd = conn.CreateCommand();
			cmd.CommandText = strSql;
			cmd.ExecuteNonQuery();
		}
		catch
		{
			b = false;
			conn.Close();
		}
	}

	return strRes;
}
#endregion
#region DeleteLocalData
public static bool DeleteLocalData(string strSql, string clientConnStr)
{
	bool b = true;
	using (SqlCeConnection conn = new SqlCeConnection(clientConnStr))
	{
		try
		{
			conn.Open();
			SqlCeCommand cmd = conn.CreateCommand();
			cmd.CommandText = strSql;
			cmd.ExecuteNonQuery();
		}
		catch
		{
			b = false;
			conn.Close();
		}
	}

	return b;
}
#endregion
#region UpdateLocalData
public static bool UpdateLocalData(string strSql, string clientConnStr)
{
	bool b = true;
	using (SqlCeConnection conn = new SqlCeConnection(clientConnStr))
	{
		try
		{
			conn.Open();
			SqlCeCommand cmd = conn.CreateCommand();
			cmd.CommandText = strSql;
			cmd.ExecuteNonQuery();
		
/* * Copyright (c) 2025, TP-Link. All rights reserved. */ package com.tplink.smb.common.data.management.system.modules.application.service.impl; import java.io.IOException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.stream.Collectors; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.tplink.smb.common.data.management.system.modules.application.constant.enums.ApplicationDataType; import com.tplink.smb.common.data.management.system.modules.application.constant.enums.ApplicationStatus; import com.tplink.smb.common.data.management.system.modules.application.constant.enums.ApplicationType; import com.tplink.smb.common.data.management.system.modules.application.constant.enums.ApprovalResult; import com.tplink.smb.common.data.management.system.modules.application.constant.enums.DeviceInfoStatus; import com.tplink.smb.common.data.management.system.modules.application.domain.ApplicationFormPO; import com.tplink.smb.common.data.management.system.modules.application.domain.VigiDeviceInfoPO; import com.tplink.smb.common.data.management.system.modules.application.domain.vo.ApplicationFormVO; import com.tplink.smb.common.data.management.system.modules.application.domain.vo.mixs.ApproverInfo; import com.tplink.smb.common.data.management.system.modules.application.repository.ApplicationFormRepository; import com.tplink.smb.common.data.management.system.modules.application.repository.OmadaDeviceInfoRepository; import com.tplink.smb.common.data.management.system.modules.application.service.ApplicationFormService; import com.tplink.smb.common.data.management.system.modules.application.service.DataInfoService; import com.tplink.smb.common.data.management.system.modules.application.service.OmadaDeviceInfoService; import com.tplink.smb.common.data.management.system.modules.application.service.VigiDeviceInfoService; import com.tplink.smb.common.data.management.system.modules.application.service.dto.ApplicationFormDto; import com.tplink.smb.common.data.management.system.modules.application.service.dto.ApplicationFormQueryCriteria; import com.tplink.smb.common.data.management.system.modules.application.service.dto.PendingApprovalDto; import com.tplink.smb.common.data.management.system.modules.application.service.mapstruct.ApplicationFormMapper; import com.tplink.smb.common.data.management.system.service.S3StorageService; import com.tplink.smb.common.data.management.system.utils.PageResult; import com.tplink.smb.common.data.management.system.utils.PageUtil; import com.tplink.smb.common.data.management.system.utils.QueryHelp; import com.tplink.smb.common.data.management.system.utils.StringUtils; /** * @author Chen Jiayuan * @version 1.0 * @since 2025/9/30 */ @Slf4j @Service @RequiredArgsConstructor public class ApplicationFormServiceImpl implements ApplicationFormService { private final ApplicationFormRepository applicationFormRepository; private final ApplicationFormMapper applicationFormMapper; private final OmadaDeviceInfoRepository omadaDeviceInfoRepository; private final OmadaDeviceInfoService omadaDeviceInfoService; private final VigiDeviceInfoService vigiDeviceInfoService; private final S3StorageService s3StorageService; /** * 将 VO 中的数据复制到 JPA Entity */ private final ObjectMapper objectMapper = new ObjectMapper(); // Jackson @Autowired private PlatformTransactionManager transactionManager; @Override public PageResult<ApplicationFormDto> queryAll( ApplicationFormQueryCriteria criteria, Pageable pageable) { Page<ApplicationFormPO> page = applicationFormRepository.findAll( (root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable); // 转换为 DTO 并添加审批信息 Page<ApplicationFormDto> dtoPage = page.map( applicationForm -> { ApplicationFormDto dto = applicationFormMapper.toDto(applicationForm); // 添加审批信息 enrichApprovalInfo(dto, applicationForm.getId()); // 添加图片预签名URL信息 enrichImageUrls(dto, applicationForm); enrichSpecificationUrl(dto, applicationForm); return dto; }); return PageUtil.toPage(dtoPage); } @Override public List<ApplicationFormDto> queryAll(ApplicationFormQueryCriteria criteria) { List<ApplicationFormPO> forms = applicationFormRepository.findAll( (root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder)); // 转换为 DTO 并添加审批信息 return forms.stream() .map( form -> { ApplicationFormDto dto = applicationFormMapper.toDto(form); // 添加审批信息 enrichApprovalInfo(dto, form.getId()); // 添加图片预签名URL信息到dataDetails中 enrichImageUrls(dto, form); enrichSpecificationUrl(dto, form); return dto; }) .collect(Collectors.toList()); } /** * 丰富申请单的审批信息 * * @param dto 申请单 DTO * @param applicationFormId 申请单 ID */ private void enrichApprovalInfo(ApplicationFormDto dto, Integer applicationFormId) { ApplicationFormPO form = applicationFormRepository.findById(applicationFormId).orElse(null); if (form != null && form.getApprovalRecords() != null) { dto.setApprovalRecords(form.getApprovalRecords()); // 直接使用 approvalRecords 字段 // 计算每个审批人的最新审批状态意见 enrichApproverStatusAndComments(dto, form); } else { dto.setApprovalRecords("[]"); } } /** * 丰富每个审批人的状态意见信息 * * @param dto 申请单 DTO * @param form 申请单实体 */ private void enrichApproverStatusAndComments(ApplicationFormDto dto, ApplicationFormPO form) { List<Map<String, Object>> records = getApprovalRecordsFromForm(form); // 获取最新轮次 int maxRound = form.getRound(); // 过滤出最新轮次的记录 List<Map<String, Object>> latestRoundRecords = records.stream() .filter(r -> (Integer) r.getOrDefault("round", 0) == maxRound) .collect(Collectors.toList()); // 为每个审批人设置状态意见 for (Map<String, Object> record : latestRoundRecords) { String approverUserName = (String) record.get("approverUserName"); Integer approvalStatus = (Integer) record.get("approvalStatus"); String comment = (String) record.get("comment"); // 根据审批人用户名匹配并设置状态意见 if (Objects.equals(approverUserName, dto.getApprover1())) { dto.setApprover1Status(approvalStatus); dto.setApprover1Comment(comment); } else if (Objects.equals(approverUserName, dto.getApprover2())) { dto.setApprover2Status(approvalStatus); dto.setApprover2Comment(comment); } else if (Objects.equals(approverUserName, dto.getApprover3())) { dto.setApprover3Status(approvalStatus); dto.setApprover3Comment(comment); } else if (Objects.equals(approverUserName, dto.getApprover4())) { dto.setApprover4Status(approvalStatus); dto.setApprover4Comment(comment); } else if (Objects.equals(approverUserName, dto.getApprover5())) { dto.setApprover5Status(approvalStatus); dto.setApprover5Comment(comment); } else if (Objects.equals(approverUserName, dto.getApprover6())) { dto.setApprover6Status(approvalStatus); dto.setApprover6Comment(comment); } } } private void enrichImageUrls(ApplicationFormDto dto, ApplicationFormPO applicationFormPO) { String dataDetails = applicationFormPO.getDataDetails(); if (StringUtils.isBlank(dataDetails)) { return; } Map<String, Object> dataMap; try { dataMap = objectMapper.readValue(dataDetails, Map.class); } catch (Exception e) { //log.warn("Failed to parse dataDetails JSON for image enrichment: {}", dataDetails, e); return; // 解析失败,跳过图片处理 } try { // 提取并解析 imageInfo Map<String, Object> imageInfoMap = parseImageInfoField(dataMap.get("imageInfo")); if (imageInfoMap == null) return; @SuppressWarnings("unchecked") Map<String, String> pathMap = (Map<String, String>) imageInfoMap.get("img_bucket_path"); if (!(pathMap instanceof Map) || pathMap.isEmpty()) return; Map<String, String> fieldMapping = Map.of( "small_img_bucket_path_for_web", "small_img_bucket_url_for_web", "big_img_bucket_path_for_web", "big_img_bucket_url_for_web", "heatmap_img_bucket_path_for_web", "heatmap_img_bucket_url_for_web", "hdpi_img_bucket_path_for_app", "hdpi_img_bucket_url_for_app" ); boolean updated = false; for (Map.Entry<String, String> entry : fieldMapping.entrySet()) { String s3Path = pathMap.get(entry.getKey()); if (StringUtils.isNotBlank(s3Path)) { String url = generatePresignedUrl(s3Path.trim()); if (url != null) { dataMap.put(entry.getValue(), url); updated = true; } } } if (updated) { dto.setDataDetails(objectMapper.writeValueAsString(dataMap)); } } catch (Exception e) { //log.warn("Unexpected error during image URL enrichment", e); // 不抛出,不影响 specification 处理 } } private void enrichSpecificationUrl(ApplicationFormDto dto, ApplicationFormPO applicationFormPO) { String dataDetails = applicationFormPO.getDataDetails(); if (StringUtils.isBlank(dataDetails)) { return; } Map<String, Object> dataMap; try { dataMap = objectMapper.readValue(dataDetails, Map.class); } catch (Exception e) { //log.warn("Failed to parse dataDetails JSON for specification enrichment: {}", dataDetails, e); return; } try { // 获取 specification 字段(S3路径) String specS3Path = (String) dataMap.get("specification"); if (StringUtils.isBlank(specS3Path)) { return; } String presignedUrl = generatePresignedUrl(specS3Path.trim()); if (presignedUrl != null) { dataMap.put("specificationUrl", presignedUrl); // 添加新字段 // 写回 DTO dto.setDataDetails(objectMapper.writeValueAsString(dataMap)); } } catch (Exception e) { //log.warn("Unexpected error during specification URL enrichment", e); // 继续执行其他流程 } } // 解析 imageInfo:支持字符串或对象 private Map<String, Object> parseImageInfoField(Object imageInfoObj) { if (imageInfoObj == null) return null; if (imageInfoObj instanceof String str && StringUtils.isNotBlank(str)) { try { return objectMapper.readValue(str.trim(), Map.class); } catch (Exception e) { //log.warn("Invalid JSON in imageInfo: {}", str, e); return null; } } else if (imageInfoObj instanceof Map<?, ?> map) { @SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) map; return result; } return null; } // 生成 S3 预签名 URL(安全封装) private String generatePresignedUrl(String s3Path) { if (StringUtils.isBlank(s3Path)) return null; try { String url = s3StorageService.generatePresignedUrl(s3Path, 3600); // 1小时有效 return StringUtils.isNotBlank(url) ? url : null; } catch (Exception e) { //log.warn("Failed to generate presigned URL for path: {}", s3Path, e); return null; } } @Override public void download(List<ApplicationFormDto> all, HttpServletResponse response) throws IOException { List<Map<String, Object>> list = new ArrayList<>(); } @Override @Transactional(rollbackFor = Exception.class) public void submitApplication(ApplicationFormVO vo) { // 查询是否已存在该申请单(根据 id 判断) ApplicationFormPO form; boolean isNew = vo.getId() == null; if (isNew) { form = new ApplicationFormPO(); form.setUuid(generateApplicationNo()); form.setRound(1); // 第一次提交,轮次为1 } else { form = applicationFormRepository .findById(vo.getId()) .orElseThrow(() -> new RuntimeException("Application Form does not exist: " + vo.getId())); // 只能不在申请流程中的状态,即草稿、已撤回、已驳回状态的申请单 if (ApplicationStatus.fromStatus(form.getStatus()).isInApprovalProcess()) { throw new RuntimeException("Only application forms in draft, withdrawn, or rejected status can be updated"); } // 从已有申请单提交,申请的轮次记录+1 form.setRound(form.getRound() + 1); // 草稿的轮次为0 只要提交就+1 } // 手动映射 VO 字段到 Entity mapVoToEntity(form, vo); // 保存申请单 form.setStatus(ApplicationStatus.SUBMITTED.getStatus()); applicationFormRepository.save(form); } /** * 生成申请单编号 格式: APP + 年月日 + 6位序列号 (例如: APP20250918000001) * * @return 申请单编号 */ private String generateApplicationNo() { // 获取当前日期 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String dateStr = sdf.format(new Date()); // 生成6位随机数或序列号 // 这里使用随机数,实际项目中可以使用数据库序列或其他方式保证唯一性 Random random = new Random(); int randomNumber = 100000 + random.nextInt(900000); // 生成100000-999999之间的数字 return "APP" + dateStr + randomNumber; } private void mapVoToEntity(ApplicationFormPO entity, ApplicationFormVO vo) { // 基础字段(标题、理由等) entity.setApplicationTitle(vo.getApplicationTitle()); entity.setApplicationReason(vo.getApplicationReason()); entity.setApplicationType(vo.getApplicationType()); entity.setApplicationDataType(vo.getApplicationDataType()); entity.setApplicationDataId(vo.getApplicationDataId()); // 设置审批人信息 entity.setApprover1(vo.getApprover1()); entity.setApprover1Level(vo.getApprover1Level()); entity.setApprover2(vo.getApprover2()); entity.setApprover2Level(vo.getApprover2Level()); entity.setApprover3(vo.getApprover3()); entity.setApprover3Level(vo.getApprover3Level()); entity.setApprover4(vo.getApprover4()); entity.setApprover4Level(vo.getApprover4Level()); entity.setApprover5(vo.getApprover5()); entity.setApprover5Level(vo.getApprover5Level()); entity.setApprover6(vo.getApprover6()); entity.setApprover6Level(vo.getApprover6Level()); // 申请人信息 entity.setApplicantUserName(vo.getApplicantUserName()); // 根据数据类型存储不同的详细信息 entity.setOriginDataDetails(vo.getOriginDataDetails()); entity.setDataDetails(vo.getDataDetails()); } // 修改 ApplicationFormServiceImpl.java 中的 withdrawApplication 方法 @Override @Transactional(rollbackFor = Exception.class) public void withdrawApplication(Integer applicationFormId, String applicantName) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("申请单不存在")); // 检查申请人是否匹配 if (!form.getApplicantUserName().equals(applicantName)) { throw new RuntimeException("只有申请人才能撤回申请单"); } // 更新状态为已撤回 form.setStatus(ApplicationStatus.WITHDRAWN.getStatus()); applicationFormRepository.save(form); } @Override @Transactional(readOnly = true) public PageResult<PendingApprovalDto> getPendingApprovals( ApplicationFormQueryCriteria criteria, Pageable pageable) { // 先查询所有申请单 List<ApplicationFormPO> allForms = applicationFormRepository.findAll(); // 过滤出包含当前审批人的申请单,减少不必要的数据库查询 List<ApplicationFormPO> filteredForms = allForms.stream() .filter( form -> ApplicationStatus.fromStatus(form.getStatus()).isInApprovalProcess()) .filter(form -> containsApprover(form, criteria.getApproverUserName())) // 先粗筛 .filter(form -> isUserCurrentApprover(form, criteria.getApproverUserName())) // 再细筛 .collect(Collectors.toList()); // 处理筛选后的申请单 List<PendingApprovalDto> result = filteredForms.stream() .map( form -> { ApplicationFormDto formDto = applicationFormMapper.toDto(form); enrichApprovalInfo(formDto, form.getId()); PendingApprovalDto dto = new PendingApprovalDto(); dto.setApplicationForm(formDto); // 直接使用表中的轮次值 dto.setRound(form.getRound()); dto.setStepOrder(getCurrentApprovalLevel(form, criteria.getApproverUserName())); return dto; }) .filter(Objects::nonNull) .collect(Collectors.toList()); // 手动分页 int total = result.size(); int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), total); List<PendingApprovalDto> pagedContent = result.subList(start, end); // 构造 PageResult(使用项目中定义的字段) PageResult<PendingApprovalDto> pageResult = new PageResult<>(); pageResult.setContent(pagedContent); pageResult.setTotalElements(total); return pageResult; } /** * 获取所有有效的审批人信息列表 */ private List<ApproverInfo> getApproverInfos(ApplicationFormPO form) { List<ApproverInfo> list = new ArrayList<>(); addIfValid(list, form.getApprover1(), form.getApprover1Level(), form.getApprover1Role()); addIfValid(list, form.getApprover2(), form.getApprover2Level(), form.getApprover2Role()); addIfValid(list, form.getApprover3(), form.getApprover3Level(), form.getApprover3Role()); addIfValid(list, form.getApprover4(), form.getApprover4Level(), form.getApprover4Role()); addIfValid(list, form.getApprover5(), form.getApprover5Level(), form.getApprover5Role()); addIfValid(list, form.getApprover6(), form.getApprover6Level(), form.getApprover6Role()); return list; } /** * 条件添加:仅当用户名非空时才加入 */ private void addIfValid(List<ApproverInfo> list, String username, Integer level, String role) { if (username != null && !username.trim().isEmpty()) { list.add(new ApproverInfo(username, level, role)); } } /** * 粗筛:判断申请单是否包含指定审批人 */ private boolean containsApprover(ApplicationFormPO form, String approverUserName) { return getApproverInfos(form).stream() .anyMatch(info -> Objects.equals(info.username(), approverUserName)); } // ApplicationFormServiceImpl.java @Override @Transactional(readOnly = true) public PageResult<ApplicationFormDto> getApprovedApplications( ApplicationFormQueryCriteria criteria, Pageable pageable) { // 先查询所有申请单 List<ApplicationFormPO> allForms = applicationFormRepository.findAll(); // 过滤出当前用户已审批的申请单 List<ApplicationFormPO> filteredForms = allForms.stream() .filter(form -> containsApprover(form, criteria.getApproverUserName())) .filter(form -> isUserApprovedApplication(form, criteria.getApproverUserName())) .filter(form -> matchesCriteria(form, criteria)) .collect(Collectors.toList()); // 转换为 ApplicationFormDto 列表 List<ApplicationFormDto> result = filteredForms.stream() .map( form -> { ApplicationFormDto dto = applicationFormMapper.toDto(form); enrichApprovalInfo(dto, form.getId()); return dto; }) .collect(Collectors.toList()); // 手动分页 int total = result.size(); int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), total); List<ApplicationFormDto> pagedContent = new ArrayList<>(); if (start < end) { pagedContent = result.subList(start, end); } // 构造 PageResult(使用项目中定义的字段) PageResult<ApplicationFormDto> pageResult = new PageResult<>(); pageResult.setContent(pagedContent); pageResult.setTotalElements(total); return pageResult; } /** * 判断用户是否已审批该申请单 */ private boolean isUserApprovedApplication(ApplicationFormPO form, String approverUserName) { List<Map<String, Object>> records = getApprovalRecordsFromForm(form); return records.stream() .anyMatch( r -> { Object approverUserNameObj = r.get("approverUserName"); Object approvalStatusObj = r.get("approvalStatus"); return approverUserNameObj != null && approvalStatusObj != null && approverUserNameObj.equals(approverUserName) && ((Integer) approvalStatusObj == ApprovalResult.APPROVE.getValue() || (Integer) approvalStatusObj == ApprovalResult.REJECT.getValue()); }); } /** * 判断申请单是否匹配查询条件 */ private boolean matchesCriteria(ApplicationFormPO form, ApplicationFormQueryCriteria criteria) { // 根据申请单UUID过滤 if (criteria.getUuid() != null && !criteria.getUuid().isEmpty()) { if (form.getUuid() == null || !form.getUuid().contains(criteria.getUuid())) { return false; } } // 根据申请人姓名过滤 if (criteria.getApplicantUserName() != null && !criteria.getApplicantUserName().isEmpty()) { if (form.getApplicantUserName() == null || !form.getApplicantUserName().contains(criteria.getApplicantUserName())) { return false; } } // 根据申请单类型过滤 if (criteria.getApplicationType() != null) { if (form.getApplicationType() == null || !form.getApplicationType().equals(criteria.getApplicationType())) { return false; } } // 根据申请单数据类型过滤 if (criteria.getApplicationDataType() != null) { if (form.getApplicationDataType() == null || !form.getApplicationDataType().equals(criteria.getApplicationDataType())) { return false; } } // 根据申请单标题过滤 if (criteria.getApplicationTitle() != null && !criteria.getApplicationTitle().isEmpty()) { return form.getApplicationTitle() != null && form.getApplicationTitle().contains(criteria.getApplicationTitle()); } return true; } @Transactional(rollbackFor = Exception.class) @Override public void approveApplication( Integer applicationFormId, String approverUserName, Integer approvalStatus, String comment) { Optional<ApplicationFormPO> formOpt = applicationFormRepository.findById(applicationFormId); if (!formOpt.isPresent()) { throw new RuntimeException("Application Form Id: " + applicationFormId + " not found"); } ApplicationFormPO form = formOpt.get(); ApplicationStatus status = ApplicationStatus.fromStatus(form.getStatus()); // 自动映射 ApplicationType applicationType = ApplicationType.fromType(form.getApplicationType()); Timestamp now = new Timestamp(System.currentTimeMillis()); // 检查申请单状态是否为已提交或待审批 if (!status.equals(ApplicationStatus.SUBMITTED) && !status.equals(ApplicationStatus.REVIEW)) { throw new RuntimeException("The application status is incorrect, and approval is not possible"); } // 检查该用户是否是当前需要审批的人 if (!isUserCurrentApprover(form, approverUserName)) { throw new RuntimeException("The current user is not the one requiring approval"); } // 获取当前审批轮次(直接从实体中获取) Integer currentRound = form.getRound(); // 如果申请单状态是已提交,将其改为待审批 if (status.equals(ApplicationStatus.SUBMITTED)) { form.setStatus(ApplicationStatus.REVIEW.getStatus()); } // 创建审批记录 Map<String, Object> record = new LinkedHashMap<>(); record.put("id", UUID.randomUUID().toString()); record.put("applicationFormId", applicationFormId); record.put("round", currentRound); record.put("approverLevel", getCurrentApprovalLevel(form, approverUserName)); record.put("approverRole", getApproverRole(form, approverUserName)); record.put("approverUserName", approverUserName); record.put("approvalStatus", approvalStatus); record.put("comment", comment); record.put("approvedAt", now); // 更新审批记录到 JSON 字段 updateApprovalRecords(form, record); // 处理审批结果 int currentApprovalLevel = getCurrentApprovalLevel(form, approverUserName); if (Objects.equals(approvalStatus, ApprovalResult.REJECT.getValue())) { // 审批被驳回,更新申请单状态 status = ApplicationStatus.fromApprovalLevel(currentApprovalLevel,false); } else { // 审批通过,检查当前层级是否全部完成 if (isCurrentLevelCompleted(form, currentRound, approverUserName)) { // 设置申请单状态为当前层级审批通过 status = ApplicationStatus.fromApprovalLevel(currentApprovalLevel,true); // 一级审核过后进入灰度发布状态 if(currentApprovalLevel == 1) { status = ApplicationStatus.SYNC_GRAY; } // 检查是否还有更高层级需要审批 if (hasNextLevel(form, currentRound)) { // 还有更高层级,保持待审批状态 } else { // 所有层级都完成,状态改为审批通过 status = ApplicationStatus.REVIEW_APPROVED; // 如果是新增修改,修改状态为正式发布 if(applicationType.equals(ApplicationType.ADD)||applicationType.equals(ApplicationType.EDIT)) { try { Map dataDetailMap = objectMapper.readValue(form.getDataDetails(), Map.class); dataDetailMap.put("status", DeviceInfoStatus.ONLINE.getStatus()); form.setDataDetails(objectMapper.writeValueAsString(dataDetailMap)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } } } } // 审批结果处理完成,存储审批状态 form.setStatus(status.getStatus()); applicationFormRepository.save(form); startAutoProcess(applicationFormId); // 如果审批通过(所有层级均通过),处理后续流程 if (status.equals(ApplicationStatus.REVIEW_APPROVED)) { // 启动新线程执行自动流程,不使用事务 new Thread( () -> { try { } catch (Exception e) { // 记录日志,不影响主流程 } }) .start(); } } // 更新审批记录 private void updateApprovalRecords(ApplicationFormPO form, Map<String, Object> record) { List<Map<String, Object>> records = new ArrayList<>(); // 如果已有记录,先解析出来 if (form.getApprovalRecords() != null && !form.getApprovalRecords().isEmpty()) { try { ObjectMapper objectMapper = new ObjectMapper(); records = objectMapper.readValue( form.getApprovalRecords(), new TypeReference<List<Map<String, Object>>>() { }); } catch (Exception e) { } } // 添加新记录 records.add(record); // 序列化回 JSON try { ObjectMapper objectMapper = new ObjectMapper(); form.setApprovalRecords(objectMapper.writeValueAsString(records)); } catch (Exception e) { // 序列化失败则设为空数组 // form.setApprovalRecords("[]"); } } // 从表单中获取审批记录 private List<Map<String, Object>> getApprovalRecordsFromForm(ApplicationFormPO form) { if (form.getApprovalRecords() == null || form.getApprovalRecords().isEmpty()) { return new ArrayList<>(); } try { ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.readValue( form.getApprovalRecords(), new TypeReference<List<Map<String, Object>>>() { }); } catch (Exception e) { return new ArrayList<>(); } } /** * 判断用户是否是当前需要审批的人 */ private boolean isUserCurrentApprover(ApplicationFormPO form, String approverUserName) { // 获取当前用户在审批流程中的层级 int currentUserLevel = getCurrentApprovalLevel(form, approverUserName); // 获取所有审批记录 List<Map<String, Object>> records = getApprovalRecordsFromForm(form); // 检查当前用户是否已经在该层级审批过(无论通过还是驳回) boolean userAlreadyApprovedInLevel = records.stream() .anyMatch( r -> { Object approverLevel = r.get("approverLevel"); Object approverUserNameObj = r.get("approverUserName"); return approverLevel != null && approverUserNameObj != null && (Integer) approverLevel == currentUserLevel && approverUserNameObj.equals(approverUserName); }); // 如果用户已在该层级审批过,则不是当前需要审批的人 if (userAlreadyApprovedInLevel) { return false; } // 确定当前应该审批的层级 // 查找最小的未完成审批的层级 int currentExpectedLevel = determineCurrentApprovalLevel(form, records); // 检查当前用户是否在应该审批的层级上 if (currentUserLevel != currentExpectedLevel) { return false; } // 获取当前层级的所有审批人 List<String> currentLevelApprovers = getCurrentLevelApprovers(form, currentUserLevel); // 检查当前用户是否是该层级的审批人 return currentLevelApprovers.contains(approverUserName); } /** * 确定当前应该审批的层级 规则:从层级1开始,检查每个层级是否所有审批人都已完成审批 如果某一层级还有未审批的审批人,则该层级为当前审批层级 如果某一层级所有审批人都已审批,则检查下一层级 */ private int determineCurrentApprovalLevel(ApplicationFormPO form, List<Map<String, Object>> records) { // 获取所有非空层级并排序 List<Integer> sortedLevels = getApproverInfos(form).stream() .map(ApproverInfo::level) .filter(Objects::nonNull) .distinct() .sorted() .toList(); if (sortedLevels.isEmpty()) { return 0; } // 遍历每个层级,检查是否已全部通过 for (Integer level : sortedLevels) { List<String> levelApprovers = getCurrentLevelApprovers(form, level); if (levelApprovers.isEmpty()) continue; long approvedCount = records.stream() .filter(r -> { Object rLevel = r.get("approverLevel"); Object status = r.get("approvalStatus"); Object usernameObj = r.get("approverUserName"); return rLevel != null && status != null && usernameObj != null && ((Integer) rLevel).equals(level) && (Integer) status == ApprovalResult.APPROVE.getValue() && levelApprovers.contains(usernameObj.toString()); }) .count(); // 只要有一个没通过,就停留在这一层 if (approvedCount < levelApprovers.size()) { return level; } } // 所有层级都通过,返回最大层级 + 1(表示流程结束或异常) return sortedLevels.get(sortedLevels.size() - 1) + 1; } /** * 获取指定层级的所有审批人 */ private List<String> getCurrentLevelApprovers(ApplicationFormPO form, int level) { return getApproverInfos(form).stream() .filter(info -> info.level() != null && info.level().equals(level)) .map(ApproverInfo::username) .toList(); } /** * 获取用户在审批流程中的层级 */ private int getCurrentApprovalLevel(ApplicationFormPO form, String approverUserName) { return getApproverInfos(form).stream() .filter(info -> Objects.equals(info.username(), approverUserName)) .map(info -> info.level() != null ? info.level() : 0) .findFirst() .orElse(0); } /** * 获取审批人角色 */ private String getApproverRole(ApplicationFormPO form, String approverUserName) { return getApproverInfos(form).stream() .filter(info -> Objects.equals(info.username(), approverUserName)) .map(ApproverInfo::role) .findFirst() .orElse("unknown"); } /** * 判断当前层级是否全部完成 */ private boolean isCurrentLevelCompleted( ApplicationFormPO form, Integer currentRound, String approverUserName) { // 获取所有审批记录 List<Map<String, Object>> records = getApprovalRecordsFromForm(form); // 获取当前轮次的记录 List<Map<String, Object>> currentRoundRecords = records.stream() .filter( r -> { Object round = r.get("round"); return round != null && round == currentRound; }) .collect(Collectors.toList()); // 获取当前审批人所在层级 int currentLevel = getCurrentApprovalLevel(form, approverUserName); // 检查当前层级是否全部审批通过 long approvedCount = currentRoundRecords.stream() .filter( r -> { Object approverLevel = r.get("approverLevel"); Object approvalStatus = r.get("approvalStatus"); return approverLevel != null && approvalStatus != null && (Integer) approverLevel == currentLevel && (Integer) approvalStatus == ApprovalResult.APPROVE.getValue(); }) .count(); // 获取当前层级的审批人数 long totalCount = getCurrentLevelApprovers(form, currentLevel).size(); return approvedCount == totalCount; } /** * 判断是否还有更高层级需要审批 */ private boolean hasNextLevel(ApplicationFormPO form, Integer currentRound) { // 获取所有审批记录 List<Map<String, Object>> records = getApprovalRecordsFromForm(form); // 获取当前轮次的记录 List<Map<String, Object>> currentRoundRecords = records.stream() .filter( r -> { Object round = r.get("round"); return round != null && round == currentRound; }) .collect(Collectors.toList()); // 获取当前最高层级 int maxLevel = currentRoundRecords.stream() .mapToInt( r -> { Object approverLevel = r.get("approverLevel"); return approverLevel != null ? (Integer) approverLevel : 0; }) .max() .orElse(0); // 检查是否有更高层级的审批人 return hasHigherLevelApprover(form, maxLevel); } /** * 判断是否有更高层级的审批人 */ private boolean hasHigherLevelApprover(ApplicationFormPO form, int currentLevel) { return getApproverInfos(form).stream() .map(ApproverInfo::level) .filter(Objects::nonNull) .anyMatch(level -> level > currentLevel); } private void startAutoProcess(Integer applicationFormId) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + applicationFormId)); // 只有新增、修改、上线、下线申请才需要自动处理 if (form.getApplicationType() >= 0 && form.getApplicationType() <= 3) { // 异步执行自动处理流程 completeProcessBasedOnType(form); } else { // 其他类型直接完成 form.setStatus(ApplicationStatus.COMPLETED.getStatus()); applicationFormRepository.save(form); } } @Override public void ApplicationPostProcess(Integer applicationFormId) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + applicationFormId)); // 根据申请数据类型处理不同类型的申请 switch (ApplicationDataType.fromDataType(form.getApplicationDataType())) { case OMADA: processApplicationData(form, omadaDeviceInfoService); break; case VIGI: processApplicationData(form, vigiDeviceInfoService); break; } } private void processApplicationData(ApplicationFormPO form, DataInfoService dataInfoService) { switch (ApplicationType.fromType(form.getApplicationType())) { case ADD: // 新增 Integer applicationDataId = dataInfoService.createFromJson(form.getDataDetails()); // 更新申请单的application_data_id form.setApplicationDataId(applicationDataId); // 检查application_data_id是否已经存在,如果存在就不更新 if (applicationDataId != null && applicationFormRepository.findByApplicationDataId(applicationDataId) == null) { applicationFormRepository.save(form); } break; case EDIT: // 修改 dataInfoService.createFromJson(form.getDataDetails()); break; case ONLINE: // 上线 dataInfoService.setDataStatus(form.getApplicationDataId(), VigiDeviceInfoPO.STATUS_ONLINE); break; case OFFLINE: // 下线 dataInfoService.setDataStatus(form.getApplicationDataId(), VigiDeviceInfoPO.STATUS_OFFLINE); break; default: throw new RuntimeException("Invalid application type: " + form.getApplicationType()); } } @Override @Transactional(rollbackFor = Exception.class) public void saveDraft(ApplicationFormVO vo) { ApplicationFormPO form; if (vo.getId() == null) { form = new ApplicationFormPO(); form.setUuid(generateApplicationNo()); form.setRound(0); form.setStatus(ApplicationStatus.DRAFT.getStatus()); } else { // 更新草稿 form = applicationFormRepository .findById(vo.getId()) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + vo.getId())); // 只能不在申请流程中的状态,即草稿、已撤回、已驳回状态的申请单 if (ApplicationStatus.fromStatus(form.getStatus()).isInApprovalProcess()) { throw new RuntimeException("Only application forms in draft, withdrawn, or rejected status can be updated"); } } // 映射VO到Entity mapVoToEntity(form, vo); ApplicationFormPO save = applicationFormRepository.save(form); // TODO 测试用,稍后要删除 处理设备信息申请 // 存储到数据库 ApplicationPostProcess(save.getId()); executeSync(save); } @Override @Transactional(rollbackFor = Exception.class) public void manualTriggerFirmwareVerify(Integer applicationFormId) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + applicationFormId)); // 检查是否可以手动触发固件校验 ApplicationStatus applicationStatus = ApplicationStatus.fromStatus(form.getStatus()); if (!applicationStatus.equals(ApplicationStatus.VERIFY_FIRMWARE_FAILED)) { throw new RuntimeException("The current status does not allow manual triggering of firmware verification"); } // 异步执行固件校验 executeAutoFirmwareVerification(form); } @Override @Transactional(rollbackFor = Exception.class) public void manualTriggerSync(Integer applicationFormId) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + applicationFormId)); // 检查是否可以手动触发同步 // 仅有等待灰度发布,灰度发布失败,正式发布失败时可以启用手动将同步 ApplicationStatus applicationStatus = ApplicationStatus.fromStatus(form.getStatus()); if (!applicationStatus.equals(ApplicationStatus.SYNC) &&!applicationStatus.equals(ApplicationStatus.SYNC_GRAY_FAILED) &&!applicationStatus.equals(ApplicationStatus.SYNC_FORMAL_FAILED)) { throw new RuntimeException("The current state does not allow manual triggering of synchronization"); } // 执行同步 executeSync(form); } // 固件校验预留方法 private void executeAutoFirmwareVerification(ApplicationFormPO form) { // TODO: 实现固件校验逻辑 try { // 模拟固件校验过程 firmwareVerification(form); // 异步执行同步 executeSync(form); } catch (Exception e) { // 记录错误日志 log.error("Firmware verify failed, error:{}", e.getMessage(),e); } } /** * 固件校验模拟方法 */ private void firmwareVerification(ApplicationFormPO form) { Random random = new Random(); // 随机生成 true 或 false,各占 50% boolean success = random.nextBoolean(); ApplicationStatus resultStatus = success ? ApplicationStatus.VERIFY_FIRMWARE_SUCCEED : ApplicationStatus.VERIFY_FIRMWARE_FAILED; form.setStatus(resultStatus.getStatus()); applicationFormRepository.save(form); } /** * 同步预留方法 */ @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void executeSync(ApplicationFormPO form) { ApplicationStatus applicationStatus = ApplicationStatus.fromStatus(form.getStatus()); ApplicationDataType applicationDataType = ApplicationDataType.fromDataType(form.getApplicationDataType()); try { // 根据申请单数据类型执行不同的同步逻辑 if (applicationDataType.equals(ApplicationDataType.OMADA)) { // 调用设备信息服务的同步方法 omadaDeviceInfoService.syncDeviceInfo( form.getApplicationDataId()); // 同步成功后完成流程 if(applicationStatus.equals(ApplicationStatus.SYNC_GRAY) ||applicationStatus.equals(ApplicationStatus.SYNC_FORMAL_FAILED)) { // 灰度发布 applicationStatus = ApplicationStatus.SYNC_GRAY_SUCCEED; }else{ // 其余情况 applicationStatus = ApplicationStatus.SYNC_FORMAL_SUCCEED; // 保存信息入库 ApplicationPostProcess(form.getId()); } } else if (applicationDataType.equals(ApplicationDataType.VIGI)) { // TODO: 实现VIGI同步逻辑 applicationStatus = ApplicationStatus.SYNC_FORMAL_SUCCEED; } else { throw new RuntimeException("Invalid data type: " + form.getApplicationDataType()); } } catch (Exception e) { // 同步失败后完的处理 if(applicationStatus.equals(ApplicationStatus.SYNC_GRAY) ||applicationStatus.equals(ApplicationStatus.SYNC_FORMAL_FAILED)) { // 灰度发布 applicationStatus = ApplicationStatus.SYNC_GRAY_FAILED; }else{ // 其余情况 applicationStatus = ApplicationStatus.SYNC_FORMAL_FAILED;; } log.error("Sync failed, error:{}", e.getMessage()); } finally { // 更新状态 form.setStatus(applicationStatus.getStatus()); applicationFormRepository.save(form); } } // 根据申请类型完成流程 private void completeProcessBasedOnType(ApplicationFormPO form) { ApplicationType applicationType = ApplicationType.fromType(form.getApplicationType()); switch (applicationType) { case ADD: // 新增 case EDIT: // 修改 // 新增修改需要固件校验同步 executeAutoFirmwareVerification(form); break; case ONLINE: // 上线 case OFFLINE: // 下线 // 上线下线只需要同步 executeSync(form); break; default: throw new RuntimeException("Invalid application type: " + form.getApplicationType()); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteApplicationForm(Integer applicationFormId, String applicantName) { ApplicationFormPO form = applicationFormRepository .findById(applicationFormId) .orElseThrow(() -> new RuntimeException("Application Form does not exist id: " + applicationFormId)); // 检查申请人是否匹配 if (!form.getApplicantUserName().equals(applicantName)) { throw new RuntimeException("Only the applicant can delete the application form"); } // 检查当前状态是否允许删除(只有草稿状态才能删除) if (!ApplicationStatus.fromStatus(form.getStatus()).equals(ApplicationStatus.DRAFT)) { throw new RuntimeException("Only application forms in draft status can be deleted"); } // 删除申请单本身(审批记录已包含在内) applicationFormRepository.deleteById(applicationFormId); } } 加了 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class),executesyn方法显示@Transactional self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime
最新发布
10-21
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值