commit
Some checks failed
API接口参数变更检测 / api-param-check (push) Has been cancelled

This commit is contained in:
2026-06-05 16:18:40 +08:00
parent 1ca34c6bb2
commit 3cba3bb74e
4393 changed files with 450030 additions and 103 deletions

View File

@@ -0,0 +1,182 @@
package jnpf.certificate.controller;
import com.github.pagehelper.PageInfo;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.certificate.service.CertificateManageService;
import jnpf.certificate.service.CertificateInstanceService;
import jnpf.model.certificate.req.CertificateHealthManageQueryReq;
import jnpf.model.certificate.req.CertificateInstanceQueryReq;
import jnpf.model.certificate.req.CertificateStoreManageQueryReq;
import jnpf.model.certificate.req.CertificateStoreDashboardReq;
import jnpf.model.certificate.req.CertificateSyncHealthReq;
import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq;
import jnpf.model.certificate.vo.CertificateHealthManageVO;
import jnpf.model.certificate.vo.CertificateInstanceVO;
import jnpf.model.certificate.vo.CertificateStoreManageVO;
import jnpf.model.certificate.vo.CertificateStoreDashboardVO;
import jnpf.model.certificate.vo.CertificateStoreCustomStatusTableVO;
import jnpf.model.certificate.vo.CertificateTypeOptionVO;
import jnpf.model.certificate.vo.app.HealthCertificateDetailVO;
import jnpf.util.FtbUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Optional;
/**
* web证照实例控制器。
*/
@RestController
@Validated
@RequestMapping("/web/certificate-instance")
public class CertificateInstanceController {
@Autowired
private CertificateInstanceService certificateInstanceService;
@Autowired
private CertificateManageService certificateManageService;
/**
* 更新健康证。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-health")
public ActionResult<Void> updateHealth(@Validated @RequestBody CertificateAppHealthCertificateUpdateReq req) {
certificateManageService.updateHealthCertificate(req);
return ActionResult.success();
}
/**
* 更新营业执照。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-business-license")
public ActionResult<Void> updateBusinessLicense(@Validated @RequestBody CertificateAppBusinessLicenseUpdateReq req) {
certificateManageService.updateBusinessLicense(req);
return ActionResult.success();
}
/**
* 更新食品经营许可证。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-hygiene-license")
public ActionResult<Void> updateHygieneLicense(@Validated @RequestBody CertificateAppHygieneLicenseUpdateReq req) {
certificateManageService.updateHygieneLicense(req);
return ActionResult.success();
}
/**
* 更新门店自定义证照。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-store-custom")
public ActionResult<Void> updateStoreCustom(@Valid @RequestBody CertificateAppStoreCustomUpdateReq req) {
certificateManageService.updateStoreCustomCertificate(req);
return ActionResult.success();
}
/**
* 按ID查询详情。
*
* @param id 实例ID
* @return 详情
*/
@GetMapping("/query-info/{id}")
public ActionResult<CertificateInstanceVO> queryInfo(@PathVariable("id") @NotBlank(message = "实例ID不能为空") String id) {
return ActionResult.success(certificateInstanceService.queryInfo(id));
}
/**
* 分页查询列表。
*
* @param req 查询参数
* @return 分页结果
*/
@GetMapping("/query-page")
@Deprecated
public ActionResult<PageListVO<CertificateInstanceVO>> queryPage(@Valid CertificateInstanceQueryReq req) {
PageInfo<CertificateInstanceVO> pageInfo = certificateInstanceService.queryPage(req);
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 健康证管理分页查询。
*
* @param req 查询参数
* @return 分页结果
*/
@PostMapping("/query-health-page")
public ActionResult<PageListVO<CertificateHealthManageVO>> queryHealthPage(@RequestBody @Valid CertificateHealthManageQueryReq req) {
PageInfo<CertificateHealthManageVO> pageInfo = certificateInstanceService.queryHealthPage(req);
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 门店证照分页查询。
*
* @param req 查询参数
* @return 分页结果
*/
@PostMapping("/query-store-page")
public ActionResult<PageListVO<CertificateStoreManageVO>> queryStorePage(@RequestBody @Valid CertificateStoreManageQueryReq req) {
PageInfo<CertificateStoreManageVO> pageInfo = certificateInstanceService.queryStorePage(req);
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 门店证照看板统计。
*
* @param req 查询参数
* @return 看板统计结果
*/
@PostMapping("/store-dashboard")
public ActionResult<CertificateStoreDashboardVO> storeDashboard(@Validated @RequestBody CertificateStoreDashboardReq req) {
return ActionResult.success(certificateInstanceService.storeCertificateDashboard(req));
}
/**
* 门店证照看板表格分页查询。
*
* @param req 查询参数
* @return 分页结果
*/
@PostMapping("/store-dashboard-table-page")
public ActionResult<PageListVO<CertificateStoreCustomStatusTableVO>> storeDashboardTablePage(@Validated @RequestBody CertificateStoreDashboardReq req) {
PageInfo<CertificateStoreCustomStatusTableVO> pageInfo = certificateInstanceService.storeCertificateDashboardTablePage(req);
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 查询证照类型选项。
*
* @return 证照类型选项
*/
@GetMapping("/query-certificate-type-list")
public ActionResult<List<CertificateTypeOptionVO>> queryCertificateTypeList() {
return ActionResult.success(certificateInstanceService.queryCertificateTypeList());
}
}

View File

@@ -0,0 +1,59 @@
package jnpf.certificate.controller;
import jnpf.base.ActionResult;
import jnpf.certificate.CertificateManageApi;
import jnpf.certificate.service.CertificateManageApiService;
import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 组织营业执照管理接口。
*/
@Validated
@RestController
@RequestMapping("/web/certificate-manage-api")
public class CertificateManageApiController implements CertificateManageApi {
@Autowired
private CertificateManageApiService certificateManageApiService;
@Override
@GetMapping("/query-business-license")
public ActionResult<CertificateOrganizeBusinessLicenseVO> queryBusinessLicense(@RequestParam("organizeId") String organizeId) {
return ActionResult.success(certificateManageApiService.queryBusinessLicense(organizeId));
}
@Override
@PostMapping("/query-business-license-batch")
public ActionResult<List<CertificateOrganizeBusinessLicenseVO>> queryBusinessLicenseBatch(@RequestBody Collection<String> organizeIds) {
return ActionResult.success(certificateManageApiService.queryBusinessLicenseBatch(organizeIds));
}
@Override
@PostMapping("/save-business-license")
public ActionResult<Void> saveBusinessLicense(@Valid @RequestBody CertificateOrganizeBusinessLicenseVO req) {
certificateManageApiService.saveBusinessLicense(req);
return ActionResult.success();
}
@Override
@DeleteMapping("/delete-business-license")
public ActionResult<Void> deleteBusinessLicense(@RequestParam("organizeId") String organizeId,
@RequestParam(value = "loginUserId", required = false) String loginUserId) {
certificateManageApiService.deleteBusinessLicense(organizeId, loginUserId);
return ActionResult.success();
}
}

View File

@@ -0,0 +1,190 @@
package jnpf.certificate.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ocr.v20181119.OcrClient;
import com.tencentcloudapi.ocr.v20181119.models.*;
import jnpf.base.ActionResult;
import jnpf.certificate.config.FoodSafetyOcrConfig;
import jnpf.model.certificate.req.CertificateFoodSafetyOcrReq;
import jnpf.model.certificate.vo.CertificateFoodSafetyOcrVO;
import jnpf.permission.vo.LicenseVo;
import jnpf.personnels.config.TengxunLicenseConfig;
import jnpf.util.JsonUtil;
import jnpf.util.RedisUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 证照 OCR 控制器。
*/
@RestController
@Validated
@RequestMapping("/web/certificate-ocr")
@Slf4j
public class CertificateOcrController {
private static final String FOOD_SAFETY_OCR_CACHE_KEY_PREFIX = "ftb:certificate:ocr:food:safety:";
private static final long FOOD_SAFETY_OCR_CACHE_SECONDS = 24 * 60 * 60L;
@Autowired
private TengxunLicenseConfig licenseConfig;
@Autowired
private FoodSafetyOcrConfig foodSafetyOcrConfig;
@Autowired
private RedisUtil redisUtil;
/**
* 食品安全许可证识别(空实现)。
*
* @param req 图片地址
* @return 识别结果
*/
@PostMapping("/recognize-food-safety-license")
public ActionResult<CertificateFoodSafetyOcrVO> recognizeFoodSafetyLicense(@Validated @RequestBody CertificateFoodSafetyOcrReq req) {
return ActionResult.success(recognizeFoodSafetyLicense(req.getImageUrl())
.orElse(new CertificateFoodSafetyOcrVO(false)));
}
private Optional<CertificateFoodSafetyOcrVO> recognizeFoodSafetyLicense(String imageUrl) {
String tenantId = UserProvider.getUser().getTenantId();
String cacheKey = buildFoodSafetyOcrCacheKey(tenantId,imageUrl);
try {
Object cached = redisUtil.getString(cacheKey);
if (cached != null && StringUtil.isNotBlank(cached.toString())) {
CertificateFoodSafetyOcrVO cacheVo = JsonUtil.getJsonToBean(cached.toString(), CertificateFoodSafetyOcrVO.class);
if (cacheVo != null) {
return Optional.of(cacheVo);
}
}
} catch (Exception e) {
log.warn("read food safety ocr cache error, imageUrl={}", imageUrl, e);
}
try{
// 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey此处还需注意密钥对的保密
// 代码泄露可能会导致 SecretId 和 SecretKey 泄露并威胁账号下所有资源的安全性。以下代码示例仅供参考建议采用更安全的方式来使用密钥请参见https://cloud.tencent.com/document/product/1278/85305
// 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
Credential cred = new Credential(licenseConfig.getSecretId(), licenseConfig.getSecretKey());
// 实例化一个http选项可选的没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(licenseConfig.getDomain());
// 实例化一个client选项可选的没有特殊需求可以跳过
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
// 实例化要请求产品的client对象,clientProfile是可选的
OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile);
// 实例化一个请求对象,每个接口都会对应一个request对象
SmartStructuralOCRRequest smartStructuralOCRRequest = new SmartStructuralOCRRequest();
smartStructuralOCRRequest.setImageUrl(imageUrl);
SmartStructuralOCRResponse resp = client.SmartStructuralOCR(smartStructuralOCRRequest);
if(Objects.isNull(resp)){
return Optional.empty();
}
StructuralItem [] structuralItems = resp.getStructuralItems();
if(structuralItems == null || structuralItems.length == 0){
return Optional.empty();
}
Set<String> titleNames = foodSafetyOcrConfig.getTitleNames();
Set<String> issueDateNames = foodSafetyOcrConfig.getIssueDateNames();
Set<String> expireDateNames = foodSafetyOcrConfig.getExpireDateNames();
Set<String> businessItemsNames = foodSafetyOcrConfig.getBusinessItemsNames();
String title = null;
String issueDate = null;
String expireDate = null;
String businessItems = null;
for (StructuralItem structuralItem : structuralItems){
String name = structuralItem.getName();
String value = structuralItem.getValue();
if(titleNames.contains(name)){
title = value;
}else if(issueDateNames.contains(name)){
issueDate = value;
} else if (expireDateNames.contains(name)) {
expireDate = value;
} else if (businessItemsNames.contains(name)) {
businessItems = value;
}
}
if(Objects.isNull(title) || Objects.isNull(issueDate) || Objects.isNull(expireDate) || Objects.isNull(businessItems)){
log.error("ocr fail title,issueDate,expireDate,businessItems is null.structuralItems:{}",JsonUtil.getObjectToString(structuralItems));
return Optional.empty();
}
CertificateFoodSafetyOcrVO result = new CertificateFoodSafetyOcrVO(issueDate, expireDate, businessItems,true);
try {
redisUtil.insert(cacheKey, JsonUtil.getObjectToString(result), FOOD_SAFETY_OCR_CACHE_SECONDS);
} catch (Exception e) {
log.warn("write food safety ocr cache error, imageUrl={}", imageUrl, e);
}
return Optional.of(result);
} catch (TencentCloudSDKException e) {
log.error("Tencent ocr error ",e);
}
return Optional.empty();
}
private String buildFoodSafetyOcrCacheKey(String tenantId,String imageUrl) {
return FOOD_SAFETY_OCR_CACHE_KEY_PREFIX +tenantId+":"+ DigestUtil.md5Hex(StrUtil.nullToEmpty(imageUrl));
}
public static void main(String[] args) {
String imgUrl = "https://img.cdn1.vip/i/69dda78d48f89_1776134029.webp";
try{
// 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey此处还需注意密钥对的保密
// 代码泄露可能会导致 SecretId 和 SecretKey 泄露并威胁账号下所有资源的安全性。以下代码示例仅供参考建议采用更安全的方式来使用密钥请参见https://cloud.tencent.com/document/product/1278/85305
// 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
Credential cred = new Credential("AKIDJTbdT7ayRuIAC848D7mKm2Ji5XHua7es", "HOI1iFakDZidu461qaEweHtNfjBY5Rfp");
// 实例化一个http选项可选的没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint("ocr.tencentcloudapi.com");
// 实例化一个client选项可选的没有特殊需求可以跳过
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
// 实例化要请求产品的client对象,clientProfile是可选的
OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile);
// 实例化一个请求对象,每个接口都会对应一个request对象
EnterpriseLicenseOCRRequest req = new EnterpriseLicenseOCRRequest();
req.setImageUrl(imgUrl);
// 返回的resp是一个BizLicenseOCRResponse的实例与请求对象对应
EnterpriseLicenseOCRResponse resp = client.EnterpriseLicenseOCR(req);
SmartStructuralOCRV2Request smartStructuralOCRV2Request = new SmartStructuralOCRV2Request();
smartStructuralOCRV2Request.setImageUrl(imgUrl);
SmartStructuralOCRV2Response smartStructuralOCRV2Response = client.SmartStructuralOCRV2(smartStructuralOCRV2Request);
SmartStructuralOCRRequest smartStructuralOCRRequest = new SmartStructuralOCRRequest();
smartStructuralOCRRequest.setImageUrl(imgUrl);
SmartStructuralOCRResponse smartStructuralOCRResponse = client.SmartStructuralOCR(smartStructuralOCRRequest);
System.out.println(resp);
System.out.println(JsonUtil.getObjectToString(smartStructuralOCRResponse.getStructuralItems()));
// 输出json格式的字符串回包
} catch (TencentCloudSDKException e) {
log.error("Tencent ocr error ",e);
}
}
}

View File

@@ -0,0 +1,75 @@
package jnpf.certificate.controller;
import io.seata.spring.annotation.GlobalTransactional;
import io.swagger.v3.oas.annotations.Operation;
import jnpf.base.ActionResult;
import jnpf.certificate.service.CertificateStoreService;
import jnpf.exception.HandleException;
import jnpf.model.certificate.req.CertificateStoreSaveReq;
import jnpf.model.certificate.vo.CertificateStoreAndCertificatesVO;
import jnpf.model.certificate.vo.CertificateStoreTabVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* web门店证照控制器。
*/
@RestController
@Validated
@RequestMapping("/web/certificate-store")
public class CertificateStoreController {
/**
* 门店证照服务。
*/
@Autowired
private CertificateStoreService certificateStoreService;
/**
* 保存门店及证照数据。
* 核心参数包含门店信息、健康证、营业执照、食品经营许可证和门店自定义证照集合。
*
* @param req 保存参数
* @return 操作结果
*/
@PostMapping("/save-store-and-certificates")
@GlobalTransactional
public ActionResult<String> saveStoreAndCertificates(@Validated @RequestBody CertificateStoreSaveReq req) {
return ActionResult.success("success",certificateStoreService.saveStoreAndCertificates(req));
}
@Operation(summary = "[删除]删除门店和证照")
@DeleteMapping(value = "/{id}")
@GlobalTransactional
public ActionResult<Boolean> deleteStore(@PathVariable("id") String id) throws HandleException {
return ActionResult.success(certificateStoreService.deleteStore(id));
}
/**
* 根据门店ID查询门店证照详情。营业执照、食品许可证、门店自定义证照集合。
*
* @param storeId 门店ID
* @return 门店证照详情
*/
@GetMapping("/get-store-certificates")
public ActionResult<CertificateStoreAndCertificatesVO> getStoreCertificates(@RequestParam("storeId")
@NotBlank(message = "门店ID不能为空")
String storeId) {
return ActionResult.success(certificateStoreService.getStoreAndCertificates(storeId));
}
/**
* 查询门店证照Tab列表。
*
* @return 门店证照Tab列表
*/
@GetMapping("/query-store-certificate-tab-list")
public ActionResult<List<CertificateStoreTabVO>> queryStoreCertificateTabList() {
return ActionResult.success(certificateStoreService.queryStoreCertificateTabList());
}
}

View File

@@ -0,0 +1,932 @@
package jnpf.certificate.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jnpf.authority.utils.PermissionsUtils;
import jnpf.base.ActionResult;
import jnpf.certificate.CertificateWarningApi;
import jnpf.certificate.helper.NoticeHelper;
import jnpf.certificate.helper.OrganizationHelper;
import jnpf.certificate.mapper.CertificateInstanceMapper;
import jnpf.model.certificate.po.CertificateInstanceEntity;
import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity;
import jnpf.model.warningnotice.enums.CertificateTypeEnum;
import jnpf.model.warningnotice.vo.WarningNoticeTargetVO;
import jnpf.model.warningnotice.vo.WarningNoticeUserConfigVO;
import jnpf.model.warningnotice.vo.WarningNoticeVO;
import jnpf.permission.UserApi;
import jnpf.permission.V2UserApi;
import jnpf.permission.dto.v2.user.QueryUserBatchDTO;
import jnpf.permission.entity.UserEntity;
import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO;
import jnpf.permission.vo.v2.user.UserBaseInfoVO;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper;
import jnpf.storecertificatephoto.service.WarningNoticeService;
import jnpf.util.CustomTenantUtil;
import jnpf.util.NoDataSourceBind;
import jnpf.util.StringUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static jnpf.certificate.helper.NoticeHelper.*;
/**
* 证照预警接口实现。
*/
@Slf4j
@RestController
@RequestMapping("/web/certificate-warning-api")
public class CertificateWarningController implements CertificateWarningApi {
private static final int STATUS_MISSING = 1;
private static final int STATUS_EXPIRED = 2;
private static final int STATUS_NEAR_EXPIRE = 3;
private static final int STATUS_NORMAL = 4;
private static final int SUBJECT_TYPE_EMPLOYEE = 1;
private static final int DEFAULT_NEAR_EXPIRE_DAYS = 30;
private static final int DEFAULT_NOTICE_FREQUENCY_DAYS = 1;
private static final int STORE_CUSTOM_TEMPLATE_DISABLED_STATUS = 0;
private static final int USER_ADMINISTRATOR_FLAG = 1;
private static final String NOTICE_USER_TYPE_POSITION = "position";
private static final String NOTICE_USER_TYPE_PERSONNEL = "personnel";
private static final String NOTICE_USER_TYPE_PERSON = "person";
@Value("${config.certificate.module.id:813679492035805253}")
private String HEALTH_CERTIFICATE_PERMISSION_MODULE_ID;
@Autowired
private CertificateInstanceMapper certificateInstanceMapper;
@Autowired
private WarningNoticeService warningNoticeService;
@Autowired
private StoreCertificatePhotoMapper storeCertificatePhotoMapper;
@Autowired
private NoticeHelper noticeHelper;
@Autowired
private V2UserApi v2UserApi;
@Autowired
private UserApi userApi;
@Autowired
private PermissionsUtils permissionsUtils;
@Autowired
private CustomTenantUtil customTenantUtil;
@Autowired
private OrganizationHelper organizationHelper;
/**
* 检查证照状态并发送临期通知:
* 1. 将正常/临期状态按规则纠正为临期或过期。
* 2. 向临期对象发送预警消息(健康证通知本人,组织/门店证照通知组织负责人)。
*
* @param tenantId 租户ID
* @return 处理结果
*/
@Override
@NoDataSourceBind
@PostMapping("/checkAndSendCertificateWarning")
public ActionResult<Boolean> checkAndSendCertificateWarning(@RequestParam("tenantId") String tenantId) {
try {
log.error("checkAndSendCertificateWarning tenantId:{}",tenantId);
customTenantUtil.checkOutTenant(tenantId);
doCheckAndSend(StrUtil.trim(tenantId));
return ActionResult.success(Boolean.TRUE);
} catch (Exception e) {
log.error("检查并发送证照预警失败tenantId={}", tenantId, e);
return ActionResult.success(Boolean.FALSE);
}
}
/**
* 执行检查与发送。
*/
private void doCheckAndSend(String tenantId) {
List<CertificateInstanceEntity> candidateList = queryCandidateCertificateList();
log.error("doCheckAndSend candidateList size:{}", CollUtil.isEmpty(candidateList) ? 0 : candidateList.size());
if (CollUtil.isEmpty(candidateList)) {
return;
}
Map<String, WarningNoticeVO> warningConfigMap = queryWarningConfigMap();
Map<String, StoreCertificatePhotoEntity> templateMap = queryTemplateMap(candidateList);
Map<String, Integer> templateNearExpireDaysMap = buildTemplateNearExpireDaysMap(templateMap);
List<CertificateInstanceEntity> needUpdateList = new ArrayList<>();
List<NearExpireNoticeTask> noticeTaskList = new ArrayList<>();
for (CertificateInstanceEntity entity : candidateList) {
if (entity == null || StrUtil.isBlank(entity.getId()) || StrUtil.isBlank(entity.getCertificateType())) {
continue;
}
WarningNoticeVO warningNoticeVO = buildWarningNoticeVO(entity, warningConfigMap);
boolean skipByExpiryReminderDays = shouldSkipWarningNoticeByExpiryReminderDays(warningNoticeVO);
boolean skipWarningNotice = isStoreCustomTemplateWarningDisabled(entity, templateMap);
int nearExpireDays = resolveNearExpireDays(entity, warningConfigMap, templateNearExpireDaysMap);
int targetStatus = calculateStatus(entity, nearExpireDays,templateMap);
if ((targetStatus == STATUS_NEAR_EXPIRE || targetStatus == STATUS_EXPIRED || targetStatus == STATUS_NORMAL)
&& !Objects.equals(entity.getStatus(), targetStatus)) {
entity.setStatus(targetStatus);
needUpdateList.add(entity);
}
int effectiveStatus = (targetStatus == STATUS_NEAR_EXPIRE || targetStatus == STATUS_EXPIRED)
? targetStatus : (entity.getStatus() == null ? STATUS_MISSING : entity.getStatus());
if (effectiveStatus != STATUS_NEAR_EXPIRE || skipWarningNotice || skipByExpiryReminderDays) {
continue;
}
Integer daysToExpire = calculateDaysToExpire(entity.getExpireDate());
if (daysToExpire == null || daysToExpire < 0) {
continue;
}
int noticeFrequencyDays = resolveNoticeFrequencyDays(warningNoticeVO);
if (!shouldSendByFrequency(daysToExpire, noticeFrequencyDays)) {
continue;
}
String templateName = null;
if(isStoreCustomCertificate(entity)){
StoreCertificatePhotoEntity storeCertificatePhotoEntity = templateMap.get(entity.getTemplateId());
if(Objects.nonNull(storeCertificatePhotoEntity)){
templateName = storeCertificatePhotoEntity.getCertificateName();
}
}
noticeTaskList.add(new NearExpireNoticeTask(entity, daysToExpire, warningNoticeVO,templateName));
}
updateCertificateStatusBatch(needUpdateList);
sendNearExpireNoticeBatch(noticeTaskList, tenantId);
}
private WarningNoticeVO buildWarningNoticeVO(CertificateInstanceEntity entity, Map<String, WarningNoticeVO> warningConfigMap) {
String certificateType = entity.getCertificateType();
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) {
return warningConfigMap.get(entity.getTemplateId());
}
return warningConfigMap.get(certificateType);
}
/**
* 查询待检查的证照数据(仅正常、临期)。
*/
private List<CertificateInstanceEntity> queryCandidateCertificateList() {
LambdaQueryWrapper<CertificateInstanceEntity> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(CertificateInstanceEntity::getEnabledMark, 0);
queryWrapper.eq(CertificateInstanceEntity::getTemplateStatus,1);
queryWrapper.in(CertificateInstanceEntity::getStatus, Arrays.asList(STATUS_NORMAL, STATUS_NEAR_EXPIRE));
queryWrapper.in(CertificateInstanceEntity::getCertificateType, Arrays.asList(
CertificateTypeEnum.HEALTH_CERTIFICATE.getType(),
CertificateTypeEnum.BUSINESS_LICENSE.getType(),
CertificateTypeEnum.HYGIENE_LICENSE.getType(),
CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()
));
queryWrapper.orderByAsc(CertificateInstanceEntity::getCertificateType);
queryWrapper.orderByAsc(CertificateInstanceEntity::getSubjectType);
queryWrapper.orderByAsc(CertificateInstanceEntity::getSubjectId);
return certificateInstanceMapper.selectList(queryWrapper);
}
/**
* 查询预警设置(健康证、营业执照、食品经营许可证、门店自定义证照)。
*/
private Map<String, WarningNoticeVO> queryWarningConfigMap() {
Map<String, WarningNoticeVO> result = new HashMap<>(4);
List<WarningNoticeVO> warningNoticeVOList = warningNoticeService.queryAll();
for (WarningNoticeVO warningNoticeVO:warningNoticeVOList){
result.put(warningNoticeVO.getTypeOrTemplateId(), warningNoticeVO);
}
return result;
}
/**
* 查询门店自定义证照模板临期天数配置。
*/
private Map<String, StoreCertificatePhotoEntity> queryTemplateMap(List<CertificateInstanceEntity> instanceList) {
List<String> templateIds = instanceList.stream()
.filter(Objects::nonNull)
.filter(entity -> StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()))
.map(CertificateInstanceEntity::getTemplateId)
.filter(StrUtil::isNotBlank)
.map(StrUtil::trim)
.distinct()
.collect(Collectors.toList());
if (CollUtil.isEmpty(templateIds)) {
return Collections.emptyMap();
}
LambdaQueryWrapper<StoreCertificatePhotoEntity> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.in(StoreCertificatePhotoEntity::getId, templateIds);
queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0);
List<StoreCertificatePhotoEntity> templateList = storeCertificatePhotoMapper.selectList(queryWrapper);
if (CollUtil.isEmpty(templateList)) {
return Collections.emptyMap();
}
Map<String, StoreCertificatePhotoEntity> result = new HashMap<>(templateList.size());
for (StoreCertificatePhotoEntity template : templateList) {
if (template == null || StrUtil.isBlank(template.getId())) {
continue;
}
result.put(StrUtil.trim(template.getId()), template);
}
return result;
}
/**
* 构建门店自定义证照模板临期天数映射。
*/
private Map<String, Integer> buildTemplateNearExpireDaysMap(Map<String, StoreCertificatePhotoEntity> templateMap) {
if (CollUtil.isEmpty(templateMap)) {
return Collections.emptyMap();
}
Map<String, Integer> result = new HashMap<>(templateMap.size());
for (Map.Entry<String, StoreCertificatePhotoEntity> entry : templateMap.entrySet()) {
String templateId = StrUtil.trim(entry.getKey());
if (StrUtil.isBlank(templateId)) {
continue;
}
StoreCertificatePhotoEntity template = entry.getValue();
Integer reminderDays = template == null ? null : template.getExpiryReminderDays();
result.put(templateId, reminderDays == null || reminderDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : reminderDays);
}
return result;
}
/**
* 按证照类型解析临期阈值天数。
*/
private int resolveNearExpireDays(CertificateInstanceEntity entity,
Map<String, WarningNoticeVO> warningConfigMap,
Map<String, Integer> templateNearExpireDaysMap) {
if (entity == null || StrUtil.isBlank(entity.getCertificateType())) {
return DEFAULT_NEAR_EXPIRE_DAYS;
}
String certificateType = StrUtil.trim(entity.getCertificateType());
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) {
String templateId = StrUtil.trim(entity.getTemplateId());
Integer templateDays = templateNearExpireDaysMap.get(templateId);
return templateDays == null || templateDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : templateDays;
}
WarningNoticeVO warningNoticeVO = warningConfigMap.get(certificateType);
Integer reminderDays = warningNoticeVO == null ? null : warningNoticeVO.getExpiryReminderDays();
return reminderDays == null || reminderDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : reminderDays;
}
/**
* 若是门店自定义证照且模板状态为0则不发送预警通知。
*/
private boolean isStoreCustomTemplateWarningDisabled(CertificateInstanceEntity entity,
Map<String, StoreCertificatePhotoEntity> templateMap) {
if (entity == null || !StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) {
return false;
}
String templateId = StrUtil.trim(entity.getTemplateId());
if (StrUtil.isBlank(templateId)) {
return false;
}
StoreCertificatePhotoEntity template = templateMap.get(templateId);
return template != null && Integer.valueOf(STORE_CUSTOM_TEMPLATE_DISABLED_STATUS).equals(template.getStatus());
}
/**
* 按证照类型解析通知频率天数。
*/
private int resolveNoticeFrequencyDays(WarningNoticeVO warningNoticeVO) {
Integer frequencyDays = warningNoticeVO == null ? null : warningNoticeVO.getNoticeFrequencyDays();
return frequencyDays == null || frequencyDays <= 0 ? DEFAULT_NOTICE_FREQUENCY_DAYS : frequencyDays;
}
/**
* 当预警配置的临期提醒天数小于等于0时跳过预警通知发送。
*/
private boolean shouldSkipWarningNoticeByExpiryReminderDays(WarningNoticeVO warningNoticeVO) {
Integer expiryReminderDays = warningNoticeVO == null ? null : warningNoticeVO.getExpiryReminderDays();
return expiryReminderDays == null || expiryReminderDays <= 0;
}
/**
* 批量更新证照状态。
*/
private void updateCertificateStatusBatch(List<CertificateInstanceEntity> needUpdateList) {
if (CollUtil.isEmpty(needUpdateList)) {
return;
}
for (CertificateInstanceEntity entity : needUpdateList) {
if (entity == null || StrUtil.isBlank(entity.getId()) || entity.getStatus() == null) {
continue;
}
CertificateInstanceEntity updateEntity = new CertificateInstanceEntity();
updateEntity.setId(entity.getId());
updateEntity.setStatus(entity.getStatus());
certificateInstanceMapper.updateById(updateEntity);
}
}
/**
* 批量发送临期通知。
*/
private void sendNearExpireNoticeBatch(List<NearExpireNoticeTask> noticeTaskList, String tenantId) {
if (CollUtil.isEmpty(noticeTaskList) || StrUtil.isBlank(tenantId)) {
return;
}
Pair<Map<String, OrganizeBaseInfoVO>,Map<String,UserBaseInfoVO>> organizeMapAndUserOrgMap = queryOrganizeInfoMap(noticeTaskList,tenantId);
Map<String, OrganizeBaseInfoVO> organizeMap = organizeMapAndUserOrgMap.getLeft();
Map<String,UserBaseInfoVO> userBaseInfoByUserId = organizeMapAndUserOrgMap.getRight();
for (NearExpireNoticeTask task : noticeTaskList) {
if (task == null || task.getEntity() == null) {
continue;
}
CertificateInstanceEntity entity = task.getEntity();
OrganizeBaseInfoVO organizeBaseInfoVO = organizeMap.get(entity.getSubjectId());
//组织被禁用,则不推送
if(!isHealthEmployeeCertificate(entity) && Objects.nonNull(organizeBaseInfoVO) && organizeBaseInfoVO.isDisabled()){
continue;
}
//如果不是健康证
if(!isHealthEmployeeCertificate(entity)){
List<String> receiverUserIds = resolveReceiverUserIds(entity, organizeMap, task.getWarningNoticeVO(), tenantId);
if(CollUtil.isEmpty(receiverUserIds)){
continue;
}
NoticeMessage noticeMessage = buildNearExpireMessage(entity, task, organizeMap,false,null);
noticeHelper.sendMessage(receiverUserIds, tenantId, noticeMessage.getTitle(), noticeMessage.getContent(), null, null, null);
continue;
}
//如果是健康证的通知
//获取健康证本人
List<String> receiverUserIds = resolveReceiverUserIds(entity, organizeMap, task.getWarningNoticeVO(), tenantId);
if (CollUtil.isNotEmpty(receiverUserIds)){
NoticeMessage noticeMessage = buildNearExpireMessage(entity, task, organizeMap,true,null);
noticeHelper.sendMessage(receiverUserIds, tenantId, noticeMessage.getTitle(), noticeMessage.getContent(), BUTTON_NAME_HANDLER, buildJumpHealthCertificateUrl(entity), MP_ID_CERTIFICATE);
}
//获取健康证所属组织的负责人
OrganizeBaseInfoVO healthEmployeeOrgLeaderReceiver = resolveHealthEmployeeOrgLeaderReceiver(entity, organizeMap, userBaseInfoByUserId);
List<String> otherReceiverUserIds = new ArrayList<>();
if(Objects.nonNull(healthEmployeeOrgLeaderReceiver)){
otherReceiverUserIds.add(healthEmployeeOrgLeaderReceiver.getLeaderId());
}
//获取配置的通知者
List<String> configUserIds = resolveWarningConfigUserIds(entity, task.getWarningNoticeVO(), tenantId);
if(CollUtil.isNotEmpty(configUserIds)){
otherReceiverUserIds.addAll(configUserIds);
}
if (CollUtil.isEmpty(otherReceiverUserIds)) {
continue;
}
//健康证风险,发给该健康证的组织负责人以及配置的人员
NoticeMessage healthEmployLeaderNoticeMessage = buildNearExpireMessage(entity, task, organizeMap,false,userBaseInfoByUserId);
if(Objects.isNull(healthEmployLeaderNoticeMessage)){
continue;
}
noticeHelper.sendMessage(otherReceiverUserIds, tenantId, healthEmployLeaderNoticeMessage.getTitle(), healthEmployLeaderNoticeMessage.getContent(), BUTTON_NAME_HANDLER, buildJumpHealthCertificateUrl(entity), MP_ID_CERTIFICATE);
}
}
private String buildJumpHealthCertificateUrl(CertificateInstanceEntity entity) {
if(Objects.isNull(entity)){
return null;
}
String subjectId = entity.getSubjectId();
if(StringUtil.isBlank(subjectId)){
return null;
}
return String.format(URL_CERTIFICATE,entity.getId());
}
/**
* 左侧 查询组织信息映射(用于拿负责人和组织名称)。 key为组织idvalue为基础组织信息。
* 右侧 如果是健康证还需要查询员工所属组织信息。key为userIdvalue为该userId的基础信息
* 包含非员工主体的组织信息,以及健康证员工所属组织的信息。
*/
private Pair<Map<String, OrganizeBaseInfoVO>,Map<String,UserBaseInfoVO>> queryOrganizeInfoMap(List<NearExpireNoticeTask> noticeTaskList,String tenantId) {
Set<String> organizeIdSet = new LinkedHashSet<>();
// 收集健康证员工的 subjectId用于查询其所属组织
Set<String> healthEmployeeUserIds = new LinkedHashSet<>();
for (NearExpireNoticeTask task : noticeTaskList) {
if (task == null || task.getEntity() == null) {
continue;
}
CertificateInstanceEntity entity = task.getEntity();
if (Integer.valueOf(SUBJECT_TYPE_EMPLOYEE).equals(entity.getSubjectType())) {
if (isHealthEmployeeCertificate(entity) && StrUtil.isNotBlank(entity.getSubjectId())) {
healthEmployeeUserIds.add(StrUtil.trim(entity.getSubjectId()));
}
continue;
}
String organizeId = StrUtil.trim(entity.getSubjectId());
if (StrUtil.isNotBlank(organizeId)) {
organizeIdSet.add(organizeId);
}
}
Map<String, UserBaseInfoVO> userBaseInfoByUserId = Collections.emptyMap();
// 查询健康证员工所属的组织ID批量SQL查询
if (CollUtil.isNotEmpty(healthEmployeeUserIds)) {
userBaseInfoByUserId = organizationHelper.buildUserBaseInfoByUserIds(healthEmployeeUserIds,tenantId);
organizeIdSet.addAll(userBaseInfoByUserId.values()
.stream()
.map(UserBaseInfoVO::getOrganizeId)
.collect(Collectors.toSet()));
}
if (CollUtil.isEmpty(organizeIdSet)) {
return Pair.of(Collections.emptyMap(),userBaseInfoByUserId);
}
return Pair.of(organizationHelper.buildBaseOrganizeVO(organizeIdSet,tenantId),userBaseInfoByUserId);
}
/**
* 解析接收人:
* 1. 健康证通知当前人员
* 2. 组织/门店证照通知组织负责人。
*/
private List<String> resolveReceiverUserIds(CertificateInstanceEntity entity,
Map<String, OrganizeBaseInfoVO> organizeMap,
WarningNoticeVO warningNoticeVO,
String tenantId) {
if (entity == null) {
return Collections.emptyList();
}
String subjectId = StrUtil.trim(entity.getSubjectId());
if (StrUtil.isBlank(subjectId)) {
return Collections.emptyList();
}
List<String> receiverUserIds = new ArrayList<>();
if (isHealthEmployeeCertificate(entity)) {
receiverUserIds.add(subjectId);
} else {
addOrganizationLeaderReceiver(entity, organizeMap, receiverUserIds);
}
if (warningNoticeVO != null && !isHealthEmployeeCertificate(entity)) {
List<String> configUserIds = resolveWarningConfigUserIds(entity, warningNoticeVO, tenantId);
if (CollUtil.isNotEmpty(configUserIds)) {
receiverUserIds.addAll(configUserIds);
}
}
return normalizeIdList(receiverUserIds);
}
/**
* 非健康证默认通知组织负责人。
*/
private void addOrganizationLeaderReceiver(CertificateInstanceEntity entity,
Map<String, OrganizeBaseInfoVO> organizeMap,
List<String> receiverUserIds) {
if (entity == null || CollUtil.isEmpty(organizeMap) || receiverUserIds == null) {
return;
}
String subjectId = StrUtil.trim(entity.getSubjectId());
if (StrUtil.isBlank(subjectId)) {
return;
}
OrganizeBaseInfoVO organize = organizeMap.get(subjectId);
if (organize == null || StrUtil.isBlank(organize.getLeaderId())) {
return;
}
receiverUserIds.add(StrUtil.trim(organize.getLeaderId()));
}
/**
* 健康证通知该员工所属组织的负责人。
*/
private OrganizeBaseInfoVO resolveHealthEmployeeOrgLeaderReceiver(CertificateInstanceEntity entity,
Map<String, OrganizeBaseInfoVO> organizeMap,
Map<String,UserBaseInfoVO> userBaseInfoByUserId) {
if (Objects.isNull(entity) || CollUtil.isEmpty(organizeMap)) {
return null;
}
if(!isHealthEmployeeCertificate(entity)){
return null;
}
String subjectId = StrUtil.trim(entity.getSubjectId());
UserBaseInfoVO userBaseInfoVO = userBaseInfoByUserId.get(subjectId);
if(Objects.isNull(userBaseInfoVO)){
log.error("resolveHealthEmployeeOrgLeaderReceiver 用户的基础为空!.subjectId:{}",subjectId);
return null;
}
String orgId = userBaseInfoVO.getOrganizeId();
if(StrUtil.isBlank(orgId)){
log.error("resolveHealthEmployeeOrgLeaderReceiver 用户的组织id为空!.subjectId:{}",subjectId);
return null;
}
OrganizeBaseInfoVO organize = organizeMap.get(orgId);
if (organize != null && StrUtil.isNotBlank(organize.getLeaderId())) {
return organize;
}
return null;
}
/**
* 解析预警配置接收人。
*/
private List<String> resolveWarningConfigUserIds(CertificateInstanceEntity entity,
WarningNoticeVO warningNoticeVO,
String tenantId) {
if (entity == null || warningNoticeVO == null || StrUtil.isBlank(tenantId)) {
return Collections.emptyList();
}
List<WarningNoticeUserConfigVO> noticeConfigList = warningNoticeVO.getNoticeConfigList();
if (CollUtil.isEmpty(noticeConfigList)) {
return Collections.emptyList();
}
boolean healthCertificate = isHealthEmployeeCertificate(entity);
List<String> receiverUserIds = new ArrayList<>();
for (WarningNoticeUserConfigVO config : noticeConfigList) {
if (config == null || StrUtil.isBlank(config.getNoticeUserType()) || CollUtil.isEmpty(config.getNoticeUserList())) {
continue;
}
String noticeUserType = StrUtil.trim(config.getNoticeUserType());
if (StrUtil.equalsAnyIgnoreCase(noticeUserType, NOTICE_USER_TYPE_POSITION)) {
receiverUserIds.addAll(resolvePositionUserIds(config.getNoticeUserList(), entity, tenantId));
continue;
}
if (StrUtil.equalsAnyIgnoreCase(noticeUserType, NOTICE_USER_TYPE_PERSONNEL, NOTICE_USER_TYPE_PERSON)) {
if (healthCertificate) {
receiverUserIds.addAll(resolveHealthPersonnelConfigUserIds(config.getNoticeUserList(), entity, tenantId));
} else {
receiverUserIds.addAll(extractTargetIds(config.getNoticeUserList()));
}
}
}
return normalizeIdList(receiverUserIds);
}
/**
* 按岗位解析通知人。健康证场景下需按数据权限过滤。
*/
private List<String> resolvePositionUserIds(List<WarningNoticeTargetVO> noticeUserList,
CertificateInstanceEntity entity,
String tenantId) {
List<String> positionIds = extractTargetIds(noticeUserList);
if (CollUtil.isEmpty(positionIds) || StrUtil.isBlank(tenantId)) {
return Collections.emptyList();
}
QueryUserBatchDTO dto = new QueryUserBatchDTO();
dto.setPositionIds(positionIds);
dto.setTenantId(tenantId);
ActionResult<List<UserBoundVO>> userInfoBatch = v2UserApi.getUserInfoBatch(dto);
if (userInfoBatch == null || !Integer.valueOf(200).equals(userInfoBatch.getCode()) || CollUtil.isEmpty(userInfoBatch.getData())) {
return Collections.emptyList();
}
List<String> positionUserIds = normalizeIdList(userInfoBatch.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()));
// 健康证场景:按数据权限过滤岗位人员
if (isHealthEmployeeCertificate(entity) && CollUtil.isNotEmpty(positionUserIds)) {
return filterByDataPermission(positionUserIds, entity, tenantId);
}
return positionUserIds;
}
/**
* 按数据权限过滤通知人员(健康证场景)。
*/
private List<String> filterByDataPermission(List<String> candidateUserIds,
CertificateInstanceEntity entity,
String tenantId) {
String subjectId = entity == null ? null : StrUtil.trim(entity.getSubjectId());
if (StrUtil.isBlank(subjectId) || StrUtil.isBlank(tenantId) || CollUtil.isEmpty(candidateUserIds)) {
return Collections.emptyList();
}
List<UserEntity> userEntityList = userApi.getUserListNoData(candidateUserIds, tenantId);
if (CollUtil.isEmpty(userEntityList)) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>();
for (UserEntity userEntity : userEntityList) {
if (userEntity == null || StrUtil.isBlank(userEntity.getId())) {
continue;
}
String userId = StrUtil.trim(userEntity.getId());
if (Integer.valueOf(USER_ADMINISTRATOR_FLAG).equals(userEntity.getIsAdministrator())) {
result.add(userId);
continue;
}
List<String> dataPermissionUserIds;
try {
dataPermissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID);
} catch (Exception ex) {
log.warn("query health certificate position user permission failed,userId={},module={}", userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID, ex);
continue;
}
if (dataPermissionUserIds == null) {
result.add(userId);
continue;
}
if (CollUtil.isNotEmpty(dataPermissionUserIds) && dataPermissionUserIds.contains(subjectId)) {
result.add(userId);
}
}
return normalizeIdList(result);
}
/**
* 健康证-按人员配置时,按数据权限过滤可通知人员。
*/
private List<String> resolveHealthPersonnelConfigUserIds(List<WarningNoticeTargetVO> noticeUserList,
CertificateInstanceEntity entity,
String tenantId) {
String subjectId = entity == null ? null : StrUtil.trim(entity.getSubjectId());
if (StrUtil.isBlank(subjectId) || StrUtil.isBlank(tenantId)) {
return Collections.emptyList();
}
List<String> targetUserIds = extractTargetIds(noticeUserList);
if (CollUtil.isEmpty(targetUserIds)) {
return Collections.emptyList();
}
List<UserEntity> userEntityList = userApi.getUserListNoData(targetUserIds, tenantId);
if (CollUtil.isEmpty(userEntityList)) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>();
for (UserEntity userEntity : userEntityList) {
if (userEntity == null || StrUtil.isBlank(userEntity.getId())) {
continue;
}
String userId = StrUtil.trim(userEntity.getId());
if (Integer.valueOf(USER_ADMINISTRATOR_FLAG).equals(userEntity.getIsAdministrator())) {
result.add(userId);
continue;
}
List<String> dataPermissionUserIds;
try {
dataPermissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID);
} catch (Exception ex) {
log.warn("query health certificate user permission failed,userId={},module={}", userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID, ex);
continue;
}
if (dataPermissionUserIds == null || CollUtil.isEmpty(dataPermissionUserIds)) {
if (dataPermissionUserIds == null) {
result.add(userId);
}
continue;
}
if (dataPermissionUserIds.contains(subjectId)) {
result.add(userId);
}
}
return normalizeIdList(result);
}
/**
* 提取配置对象ID。
*/
private List<String> extractTargetIds(List<WarningNoticeTargetVO> noticeUserList) {
if (CollUtil.isEmpty(noticeUserList)) {
return Collections.emptyList();
}
return normalizeIdList(noticeUserList.stream().map(WarningNoticeTargetVO::getId).collect(Collectors.toList()));
}
/**
* 标准化ID列表。
*/
private List<String> normalizeIdList(Collection<String> idList) {
if (CollUtil.isEmpty(idList)) {
return Collections.emptyList();
}
return idList.stream()
.filter(StrUtil::isNotBlank)
.map(StrUtil::trim)
.distinct()
.collect(Collectors.toList());
}
/**
* 判断是否健康证+人员主体。
*/
private boolean isHealthEmployeeCertificate(CertificateInstanceEntity entity) {
return entity != null
&& StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.HEALTH_CERTIFICATE.getType())
&& Integer.valueOf(SUBJECT_TYPE_EMPLOYEE).equals(entity.getSubjectType());
}
/**
* 判断是否自定义证照
*/
private boolean isStoreCustomCertificate(CertificateInstanceEntity entity) {
return entity != null
&& StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType());
}
/**
* 计算状态。
*/
private int calculateStatus(CertificateInstanceEntity entity, Integer nearExpireDays,Map<String, StoreCertificatePhotoEntity> templateMap) {
Integer isLongTerm = entity.getIsLongTerm();
Date expireDate = entity.getExpireDate();
String certificateType = entity.getCertificateType();
String templateId = entity.getTemplateId();
StoreCertificatePhotoEntity storeCertificatePhotoEntity = templateMap.get(templateId);
if(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType().equals(certificateType) &&//自定义证照如果临期提醒设置为0则计算为正常
Objects.nonNull(storeCertificatePhotoEntity) &&
Integer.valueOf(0).equals(nearExpireDays)){
return STATUS_NORMAL;
}
if (Integer.valueOf(1).equals(isLongTerm)) {
return STATUS_NORMAL;
}
if (expireDate == null) {
return STATUS_MISSING;
}
Date today = DateUtil.beginOfDay(DateUtil.date());
Date target = DateUtil.beginOfDay(expireDate);
if (target.before(today)) {
return STATUS_EXPIRED;
}
int threshold = nearExpireDays == null || nearExpireDays < 0 ? 0 : nearExpireDays;
long daysDiff = DateUtil.betweenDay(today, target, false);
return daysDiff <= threshold ? STATUS_NEAR_EXPIRE : STATUS_NORMAL;
}
/**
* 计算距离到期天数。
*/
private Integer calculateDaysToExpire(Date expireDate) {
if (expireDate == null) {
return null;
}
Date today = DateUtil.beginOfDay(DateUtil.date());
Date target = DateUtil.beginOfDay(expireDate);
return (int) DateUtil.betweenDay(today, target, false);
}
/**
* 按通知频率判断是否发送。
*/
private boolean shouldSendByFrequency(Integer daysToExpire, Integer noticeFrequencyDays) {
if (daysToExpire == null || daysToExpire < 0) {
return false;
}
int frequency = noticeFrequencyDays == null || noticeFrequencyDays <= 0 ? DEFAULT_NOTICE_FREQUENCY_DAYS : noticeFrequencyDays;
return frequency <= 1 || daysToExpire % frequency == 0;
}
/**
* 构建临期通知文案。
*/
private NoticeMessage buildNearExpireMessage(CertificateInstanceEntity entity,
NearExpireNoticeTask task,
Map<String, OrganizeBaseInfoVO> organizeMap,
boolean healthCertificateReceiverBySelf,
Map<String,UserBaseInfoVO> userBaseInfoByUserId) {
Integer daysToExpire = task.getDaysToExpire();
String certificateName = resolveCertificateName(entity == null ? null : entity.getCertificateType(),task.getCertificateTemplateName());
int remainDays = daysToExpire == null ? 0 : Math.max(daysToExpire, 0);
if(isHealthEmployeeCertificate(entity)){
return buildHealthCertificateNearExpireMessage(entity,healthCertificateReceiverBySelf,userBaseInfoByUserId,remainDays);
}
String subjectName = "-";
if (entity != null && StrUtil.isNotBlank(entity.getSubjectId())) {
OrganizeBaseInfoVO organize = organizeMap.get(StrUtil.trim(entity.getSubjectId()));
if (organize != null && StrUtil.isNotBlank(organize.getName())) {
subjectName = StrUtil.trim(organize.getName());
}
}
String title = subjectName + certificateName + "即将到期,请及时处理";
String content;
if(remainDays <= 0){
content = String.format("%s%s今天到期请及时更新。", subjectName, certificateName);
}else{
content = String.format("%s%s离到期时间还有%s天请及时更新。", subjectName, certificateName, remainDays);
}
return new NoticeMessage(title, content);
}
private NoticeMessage buildHealthCertificateNearExpireMessage(CertificateInstanceEntity entity,
boolean receiverBySelf,
Map<String,UserBaseInfoVO> userBaseInfoByUserId,
int remainDays) {
if(receiverBySelf){
return new NoticeMessage("您的健康证存在风险,请及时处理", buildHealthCertificateNearExpireContent(remainDays));
}
UserBaseInfoVO baseInfoVO = userBaseInfoByUserId.get(entity.getSubjectId());
if(Objects.isNull(baseInfoVO)){
log.error("buildNearExpireMessage 通知健康证所属负责人该健康证的用户名称获取为空。entity:{}",entity);
return null;
}
return new NoticeMessage("您管理组织的健康证存在风险,请及时处理", buildNearExpireContentByNoSelf(baseInfoVO.getUserName(),remainDays));
}
private String buildHealthCertificateNearExpireContent(int remainDays) {
if(remainDays <= 0){
return "您的健康证今天过期,请及时更新健康证。";
}
return String.format("您的健康证离到期时间还有%s天请及时更新健康证。", remainDays);
}
private String buildNearExpireContentByNoSelf(String username, int remainDays) {
if(remainDays <= 0){
return String.format("%s的健康证今天过期请及时更新健康证。",username);
}
return String.format("%s的健康证离到期时间还有%s天请及时更新健康证。",username,remainDays);
}
/**
* 证照类型转中文名称。
*/
private String resolveCertificateName(String certificateType,String certificateTemplateName) {
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType())) {
return "健康证";
}
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType())) {
return "营业执照";
}
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HYGIENE_LICENSE.getType())) {
return "食品经营许可证";
}
if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) {
return certificateTemplateName;
}
return "证照";
}
/**
* 临期通知任务。
*/
@Data
private static class NearExpireNoticeTask {
private final CertificateInstanceEntity entity;
private final Integer daysToExpire;
private final WarningNoticeVO warningNoticeVO;
private final String certificateTemplateName;
private NearExpireNoticeTask(CertificateInstanceEntity entity, Integer daysToExpire, WarningNoticeVO warningNoticeVO,String certificateTemplateName) {
this.entity = entity;
this.daysToExpire = daysToExpire;
this.warningNoticeVO = warningNoticeVO;
this.certificateTemplateName = certificateTemplateName;
}
private CertificateInstanceEntity getEntity() {
return entity;
}
private Integer getDaysToExpire() {
return daysToExpire;
}
private WarningNoticeVO getWarningNoticeVO() {
return warningNoticeVO;
}
}
/**
* 通知文案。
*/
private static class NoticeMessage {
private final String title;
private final String content;
private NoticeMessage(String title, String content) {
this.title = title;
this.content = content;
}
private String getTitle() {
return title;
}
private String getContent() {
return content;
}
}
}

View File

@@ -0,0 +1,112 @@
package jnpf.certificate.controller.app;
import jnpf.base.ActionResult;
import jnpf.certificate.service.CertificateInstanceService;
import jnpf.certificate.service.CertificateManageService;
import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq;
import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq;
import jnpf.model.certificate.vo.app.CertificateAppCertificateDetailVO;
import jnpf.model.certificate.vo.app.HealthCertificateDetailVO;
import jnpf.model.certificate.vo.app.HealthCertificateVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.Optional;
/**
* App端证照管理控制器。
*/
@RestController
@Validated
@RequestMapping("/app/certificate-manage")
public class AppCertificateManageController {
/**
* App端证照管理服务。
*/
@Autowired
private CertificateManageService certificateManageService;
@Autowired
private CertificateInstanceService certificateInstanceService;
/**
* 根据证照实例ID查询证照详情。
* 返回结果中包含certificateType并按类型返回不同明细对象。
*
* @param certificateInstanceId 证照实例ID
* @return 证照详情
*/
@GetMapping("/query-info")
public ActionResult<CertificateAppCertificateDetailVO> queryInfo(@RequestParam("certificateInstanceId")
@NotBlank(message = "证照实例ID不能为空")
String certificateInstanceId) {
return ActionResult.success(certificateManageService.queryInfo(certificateInstanceId));
}
/**
* 更新健康证。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-health")
public ActionResult<Void> updateHealth(@Validated @RequestBody CertificateAppHealthCertificateUpdateReq req) {
certificateManageService.updateHealthCertificate(req);
return ActionResult.success();
}
/**
* 查询某个人的健康证信息
*
* @return 健康证信息
*/
@GetMapping("/query-health-certificate/{userId}")
public ActionResult<HealthCertificateVO> getHealthCertificateDetail(@PathVariable("userId") String userId) {
Optional<HealthCertificateDetailVO> healthCertificateDetailVOOptional = certificateInstanceService.getHealthCertificateDetail(userId);
return healthCertificateDetailVOOptional.map(h->ActionResult.success(HealthCertificateVO.of(h)))
.orElseGet(() -> ActionResult.fail(404, "未查询到健康证信息"));
}
/**
* 更新营业执照。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-business-license")
public ActionResult<Void> updateBusinessLicense(@Validated @RequestBody CertificateAppBusinessLicenseUpdateReq req) {
certificateManageService.updateBusinessLicense(req);
return ActionResult.success();
}
/**
* 更新食品经营许可证。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-hygiene-license")
public ActionResult<Void> updateHygieneLicense(@Validated @RequestBody CertificateAppHygieneLicenseUpdateReq req) {
certificateManageService.updateHygieneLicense(req);
return ActionResult.success();
}
/**
* 更新门店自定义证照。
*
* @param req 更新参数
* @return 操作结果
*/
@PutMapping("/update-store-custom")
public ActionResult<Void> updateStoreCustom(@Valid @RequestBody CertificateAppStoreCustomUpdateReq req) {
certificateManageService.updateStoreCustomCertificate(req);
return ActionResult.success();
}
}

View File

@@ -0,0 +1,83 @@
package jnpf.certificate.controller.app;
import jnpf.base.ActionResult;
import jnpf.certificate.service.CertificateAppReminderService;
import jnpf.model.certificate.req.app.CertificateAppBatchRemindReq;
import jnpf.model.certificate.req.app.CertificateAppSingleRemindReq;
import jnpf.util.UserProvider;
import jnpf.util.context.ThreadContext;
import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* App端证照提醒控制器。
*/
@RestController
@Validated
@RequestMapping("/app/certificate-reminder")
public class AppCertificateReminderController {
/**
* 证照提醒服务。
*/
@Autowired
private CertificateAppReminderService certificateAppReminderService;
@Autowired
private ThreadPoolTaskExecutor commonExecutor;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String REMIND_LOCK_KEY_PREFIX = "certificate-reminder:";
/**
* 一键提醒。
*
* @param req 提醒参数(主体类型)
* @return 操作结果
*/
@PostMapping("/batch-remind")
public ActionResult<Void> batchRemind(@Validated @RequestBody CertificateAppBatchRemindReq req) {
Boolean succ = stringRedisTemplate.opsForValue().setIfAbsent(buildRemindLockKey(),"1", 10, TimeUnit.SECONDS);
if(Objects.isNull(succ) || !succ){
return ActionResult.fail("操作太快了,请稍后在操作!");
}
//可能数据过多,但由于内部过滤数据,无法通过单独的线程执行,所以先暂时这样。
certificateAppReminderService.batchRemind(req);
return ActionResult.success();
}
/**
* 单个提醒。
*
* @param req 提醒参数证照实例ID
* @return 操作结果
*/
@PostMapping("/single-remind")
public ActionResult<Void> singleRemind(@Validated @RequestBody CertificateAppSingleRemindReq req) {
Boolean succ = stringRedisTemplate.opsForValue().setIfAbsent(buildRemindLockKey(),"1", 10, TimeUnit.SECONDS);
if(Objects.isNull(succ) || !succ){
return ActionResult.fail("操作太快了,请稍后在操作!");
}
certificateAppReminderService.singleRemind(req);
return ActionResult.success();
}
private String buildRemindLockKey() {
String userId = UserProvider.getLoginUserId();
return REMIND_LOCK_KEY_PREFIX + userId;
}
}

View File

@@ -0,0 +1,88 @@
package jnpf.certificate.controller.app;
import com.github.pagehelper.PageInfo;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.certificate.service.CertificateAppRiskService;
import jnpf.model.certificate.req.app.CertificateAppEmployeeRiskQueryReq;
import jnpf.model.certificate.req.app.CertificateAppRiskChartReq;
import jnpf.model.certificate.req.app.CertificateAppStoreRiskQueryReq;
import jnpf.model.certificate.vo.app.CertificateAppEmployeeRiskVO;
import jnpf.model.certificate.vo.app.CertificateAppRiskChartVO;
import jnpf.model.certificate.vo.app.CertificateAppRiskReminderCountVO;
import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO;
import jnpf.util.FtbUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
/**
* App端证照风险管理控制器。
*/
@RestController
@Validated
@RequestMapping("/app/certificate-risk")
public class AppCertificateRiskController {
/**
* 证照风险服务。
*/
@Autowired
private CertificateAppRiskService certificateAppRiskService;
/**
* 查询风险图表统计。
*
* @param req 查询参数(组织门店、证照类型)
* @return 风险图表统计数据
*/
@GetMapping("/query-chart")
public ActionResult<CertificateAppRiskChartVO> queryChart(@Valid CertificateAppRiskChartReq req) {
return ActionResult.success(certificateAppRiskService.queryChart(req));
}
/**
* 分页查询员工证照风险列表。
*
* @param req 查询参数(组织门店、分页)
* @return 员工证照风险分页结果(含分页信息与列表)
*/
@GetMapping("/query-employee-page")
public ActionResult<PageListVO<CertificateAppEmployeeRiskVO>> queryEmployeePage(@Valid CertificateAppEmployeeRiskQueryReq req) {
PageInfo<CertificateAppEmployeeRiskVO> pageInfo = certificateAppRiskService.queryEmployeePage(req);
// 返回员工证照风险分页数据。
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 分页查询门店证照风险列表。
* 默认仅查询缺失、过期、临期状态1、2、3
*
* @param req 查询参数(组织门店、证照类型、分页)
* @return 门店证照风险分页结果
*/
@GetMapping("/query-store-page")
public ActionResult<PageListVO<CertificateAppStoreRiskVO>> queryStorePage(@Valid CertificateAppStoreRiskQueryReq req) {
PageInfo<CertificateAppStoreRiskVO> pageInfo = certificateAppRiskService.queryStorePage(req);
return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo));
}
/**
* 查询风险提醒总数量。
* 无入参,统计缺失、临期、过期的员工风险数量和组织风险数量。
*
* @return 风险提醒数量统计
*/
@GetMapping("/query-reminder-count")
public ActionResult<CertificateAppRiskReminderCountVO> queryReminderCount(@RequestParam(value = "orgId",required = false)String orgId) {
return ActionResult.success(certificateAppRiskService.queryRiskReminderCount(orgId));
}
}