@@ -163,11 +163,6 @@ | |||
<artifactId>nd-cache-starter</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.ningdatech</groupId> | |||
<artifactId>nd-yxt-starter</artifactId> | |||
<version>1.0.0</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.alibaba</groupId> | |||
<artifactId>easyexcel-core</artifactId> | |||
</dependency> | |||
@@ -0,0 +1,184 @@ | |||
package com.hz.pm.api.common.helper; | |||
import cn.hutool.core.lang.Assert; | |||
import cn.hutool.core.util.PhoneUtil; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.hz.pm.api.external.MobileCallClient; | |||
import com.hz.pm.api.external.MobileCallClient.ReplyCallDTO; | |||
import com.hz.pm.api.external.MobileCallClient.SendCallDTO; | |||
import com.hz.pm.api.external.model.dto.CallRetDTO; | |||
import com.hz.pm.api.external.sms.MhSmsClient; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsSendDTO; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.hz.pm.api.sys.model.enumeration.BizTypeEnum; | |||
import com.hz.pm.api.sys.model.enumeration.SubmitTypeEnum; | |||
import com.hz.pm.api.sys.service.IMsgCallRecordService; | |||
import com.ningdatech.basic.exception.BizException; | |||
import lombok.RequiredArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.stereotype.Component; | |||
import org.springframework.web.servlet.View; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.stream.Collectors; | |||
import static com.hz.pm.api.sys.model.entity.MsgCallRecord.ReplyStatus; | |||
import static com.hz.pm.api.sys.model.entity.MsgCallRecord.SendStatus; | |||
/** | |||
* <p> | |||
* MsgCallHelper | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 15:49 2024/4/25 | |||
*/ | |||
@Component | |||
@Slf4j | |||
@RequiredArgsConstructor | |||
public class MsgCallHelper { | |||
private final IMsgCallRecordService msgCallRecordService; | |||
private final View error; | |||
/** | |||
* 发送短信 | |||
* | |||
* @param phone 手机号 | |||
* @param content 短信内容 | |||
* @param bizType 业务类型 | |||
* @return submitKey | |||
**/ | |||
public String sendMsg(String phone, String content, BizTypeEnum bizType) { | |||
return sendMsg(Collections.singletonList(phone), content, bizType); | |||
} | |||
/** | |||
* 批量发送短信 | |||
* | |||
* @param phones 手机号 | |||
* @param content 短信内容 | |||
* @param bizType 业务类型 | |||
* @return submitKey | |||
**/ | |||
public String sendMsg(Collection<String> phones, String content, BizTypeEnum bizType) { | |||
log.info("sendMsgCall:{},{}", phones, content); | |||
Assert.notEmpty(phones, "手机号不能为空"); | |||
phones.forEach(w -> { | |||
if (!PhoneUtil.isMobile(w)) { | |||
throw BizException.wrap("手机号格式错误:%s", w); | |||
} | |||
}); | |||
SmsDTO<SmsSendDTO> ret = MhSmsClient.getClient().smsSend(content, phones); | |||
if (!ret.isOk()) { | |||
log.error("短信发送失败:{}", ret); | |||
throw BizException.wrap("短信发送失败"); | |||
} | |||
SmsSendDTO data = ret.getData(); | |||
List<String> errorPhones = StrUtil.split(data.getErrorPhone(), ","); | |||
List<MsgCallRecord> records = phones.stream().map(w -> { | |||
MsgCallRecord mcr = new MsgCallRecord(); | |||
mcr.setContent(content); | |||
mcr.setReceivePhone(w); | |||
mcr.setSubmitKey(data.getResult()); | |||
mcr.setBizType(bizType); | |||
mcr.setSubmitType(SubmitTypeEnum.SMS); | |||
mcr.setSendStatus(errorPhones.contains(w) ? SendStatus.SUCCESS : SendStatus.FAILED); | |||
buildReplyStatusBySend(mcr, bizType); | |||
return mcr; | |||
}).collect(Collectors.toList()); | |||
msgCallRecordService.saveBatch(records); | |||
return data.getResult(); | |||
} | |||
private void buildReplyStatusBySend(MsgCallRecord mcr, BizTypeEnum bizType) { | |||
if (SendStatus.SUCCESS.equals(mcr.getSendStatus())) { | |||
switch (bizType) { | |||
case EXPERT_INVITE: | |||
mcr.setNeedReply(Boolean.TRUE); | |||
mcr.setReplyStatus(ReplyStatus.WAITING); | |||
break; | |||
default: | |||
mcr.setNeedReply(Boolean.FALSE); | |||
break; | |||
} | |||
} | |||
} | |||
/** | |||
* 获取短信回复内容 | |||
* | |||
* @param submitKey submitKey | |||
* @return 回复内容 | |||
*/ | |||
public SmsReplyDTO smsReply(String submitKey) { | |||
return MhSmsClient.getClient().smsReply(submitKey); | |||
} | |||
/** | |||
* 批量提交电话任务 | |||
* | |||
* @param phones 手机号 | |||
* @param content 内容 | |||
* @param bizType 业务类型 | |||
* @return submitKey submitKey | |||
*/ | |||
public String sendCall(Collection<String> phones, String content, BizTypeEnum bizType) { | |||
log.info("sendCall:{},{}", phones, content); | |||
Assert.notEmpty(phones, "手机号不能为空"); | |||
phones.forEach(phone -> { | |||
if (!PhoneUtil.isMobile(phone)) { | |||
throw BizException.wrap("手机号格式错误:%s", phone); | |||
} | |||
}); | |||
CallRetDTO<SendCallDTO> ret = MobileCallClient.getClient().sendCall(phones, content); | |||
if (!ret.isOk()) { | |||
log.error("电话拨打失败:{}", ret); | |||
throw BizException.wrap("电话拨打失败"); | |||
} | |||
SendCallDTO data = ret.getData(); | |||
List<MsgCallRecord> records = phones.stream().map(w -> { | |||
MsgCallRecord mcr = new MsgCallRecord(); | |||
mcr.setContent(content); | |||
mcr.setReceivePhone(w); | |||
mcr.setSubmitKey(data.getRequestId()); | |||
mcr.setBizType(bizType); | |||
mcr.setSubmitType(SubmitTypeEnum.CALL); | |||
mcr.setSendStatus(SendStatus.SUCCESS); | |||
buildReplyStatusBySend(mcr, bizType); | |||
return mcr; | |||
}).collect(Collectors.toList()); | |||
msgCallRecordService.saveBatch(records); | |||
return data.getRequestId(); | |||
} | |||
/** | |||
* 提交电话任务 | |||
* | |||
* @param phone 手机号 | |||
* @param content 内容 | |||
* @param bizType 业务类型 | |||
* @return submitKey submitKey | |||
*/ | |||
public String sendCall(String phone, String content, BizTypeEnum bizType) { | |||
return sendCall(Collections.singletonList(phone), content, bizType); | |||
} | |||
public List<ReplyCallDTO> callReply(String submitKey) { | |||
CallRetDTO<List<ReplyCallDTO>> retCallReplies = MobileCallClient.getClient().replyCall(submitKey); | |||
if (retCallReplies.isOk()) { | |||
List<ReplyCallDTO> replies = retCallReplies.getData(); | |||
if (replies == null || replies.isEmpty()) { | |||
return Collections.emptyList(); | |||
} | |||
return replies; | |||
} | |||
log.error("获取电话回执失败:{} {}", submitKey, retCallReplies); | |||
throw BizException.wrap("获取电话回执失败"); | |||
} | |||
} |
@@ -2,8 +2,7 @@ package com.hz.pm.api.expert.service.impl; | |||
import cn.hutool.core.bean.BeanUtil; | |||
import cn.hutool.core.collection.CollUtil; | |||
import cn.hutool.core.util.ObjectUtil; | |||
import cn.hutool.core.util.PhoneUtil; | |||
import cn.hutool.json.JSONUtil; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | |||
@@ -24,8 +23,7 @@ import com.hz.pm.api.expert.model.bo.ExpertInfoSensitiveFieldCheckBO; | |||
import com.hz.pm.api.expert.model.cmd.*; | |||
import com.hz.pm.api.expert.model.dto.*; | |||
import com.hz.pm.api.expert.service.*; | |||
import com.hz.pm.api.expert.utils.SensitiveModifySegmentUtils; | |||
import com.hz.pm.api.meeting.helper.YxtClientHelper; | |||
import com.hz.pm.api.expert.utils.SensitiveModifySegmentUtil; | |||
import com.hz.pm.api.meta.constant.ExpertDictTypeEnum; | |||
import com.hz.pm.api.meta.model.ExpertRegionInfo; | |||
import com.hz.pm.api.meta.model.entity.ExpertDictionary; | |||
@@ -36,19 +34,16 @@ import com.hz.pm.api.sys.model.entity.Role; | |||
import com.hz.pm.api.sys.model.entity.UserRole; | |||
import com.hz.pm.api.sys.service.IRoleService; | |||
import com.hz.pm.api.sys.service.IUserRoleService; | |||
import com.hz.pm.api.user.model.enumeration.UserAvailableEnum; | |||
import com.hz.pm.api.user.model.entity.UserInfo; | |||
import com.hz.pm.api.user.model.enumeration.RoleEnum; | |||
import com.hz.pm.api.user.security.model.UserInfoDetails; | |||
import com.hz.pm.api.user.model.enumeration.UserAvailableEnum; | |||
import com.hz.pm.api.user.service.IUserInfoService; | |||
import com.hz.pm.api.user.util.LoginUserUtil; | |||
import com.ningdatech.basic.exception.BizException; | |||
import com.ningdatech.basic.util.CollUtils; | |||
import com.ningdatech.yxt.utils.JSONUtils; | |||
import lombok.RequiredArgsConstructor; | |||
import org.apache.commons.collections4.CollectionUtils; | |||
import org.apache.commons.lang3.StringUtils; | |||
import org.springframework.beans.factory.annotation.Value; | |||
import org.springframework.stereotype.Component; | |||
import org.springframework.transaction.annotation.Transactional; | |||
@@ -57,10 +52,6 @@ import java.util.*; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import static com.hz.pm.api.expert.model.constant.ExpertAuditMsgTemplate.EXPERT_AUDIT_FAIL; | |||
import static com.hz.pm.api.expert.model.constant.ExpertAuditMsgTemplate.EXPERT_AUDIT_PASS; | |||
import static com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
/** | |||
* @author liuxinxin | |||
* @date 2022/7/22 下午4:39 | |||
@@ -79,16 +70,12 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
private final IUserRoleService userRoleService; | |||
private final IExpertSensitiveInfoModifyDetailRecordService iExpertSensitiveInfoModifyDetailRecordService; | |||
private final IUserInfoService userInfoService; | |||
private final YxtClientHelper yxtClientHelper; | |||
private final IExpertGovBusinessStripService expertGovBusinessStripService; | |||
@Value("${login.url:}") | |||
private String loginUrl; | |||
/** | |||
* 用户第一次申请修改,仅在专家用户状态为applying 状态时调用 | |||
* | |||
* @param cmd | |||
* @param cmd \ | |||
*/ | |||
@Override | |||
@Transactional(rollbackFor = Exception.class) | |||
@@ -365,8 +352,6 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
public void expertStorageDeal(ExpertStorageDealCmd cmd) { | |||
Long expertUserId = cmd.getExpertUserId(); | |||
ExpertUserFullInfo expertInfo = iExpertUserFullInfoService.getByUserId(expertUserId); | |||
UserInfoDetails userDetail = LoginUserUtil.loginUserDetail(); | |||
String content; | |||
if (Boolean.TRUE.equals(cmd.getApplyResult())) { | |||
// 修改专家状态为可用 | |||
// 账号启用 | |||
@@ -396,17 +381,8 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
userRoleService.save(saveUserRole); | |||
} | |||
} | |||
content = String.format(EXPERT_AUDIT_PASS, expertInfo.getExpertName(), loginUrl, userDetail.getRealName(), userDetail.getMobile()); | |||
} else { | |||
content = String.format(EXPERT_AUDIT_FAIL, expertInfo.getExpertName(), userDetail.getRealName(), userDetail.getMobile()); | |||
} | |||
iExpertUserFullInfoService.saveOrUpdate(expertInfo); | |||
if (PhoneUtil.isMobile(expertInfo.getPhoneNo())) { | |||
SendSmsContext smsCtx = new SendSmsContext(); | |||
smsCtx.setContent(content); | |||
smsCtx.setReceiveNumber(expertInfo.getPhoneNo()); | |||
yxtClientHelper.sendSms(smsCtx); | |||
} | |||
} | |||
@@ -631,8 +607,8 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
* 创建层级变更审核 | |||
* | |||
* @param expertRegionFieldSegmentList \ | |||
* @param originalExpertUserFullInfo \ | |||
* @param modifyApplyExtraInfo \ | |||
* @param originalExpertUserFullInfo \ | |||
* @param modifyApplyExtraInfo \ | |||
*/ | |||
private Long createExpertRegionModifyApply(List<SensitiveModifySegment> expertRegionFieldSegmentList | |||
, ExpertUserFullInfo originalExpertUserFullInfo, ModifyApplyExtraInfoDTO modifyApplyExtraInfo) { | |||
@@ -674,7 +650,7 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
Boolean applyResult = expertInfoModifyApplyDealCmd.getApplyResult(); | |||
Long expertUserId = expertInfoModifyApplyDealCmd.getExpertUserId(); | |||
Long applyId = expertInfoModifyApplyDealCmd.getApplyId(); | |||
if (applyResult) { | |||
if (Boolean.TRUE.equals(applyResult)) { | |||
ExpertUserFullInfo originalExpertUserFullInfo = iExpertUserFullInfoService.getByUserId(expertUserId); | |||
LambdaQueryWrapper<ExpertSensitiveInfoModifyDetailRecord> recordQuery = Wrappers.lambdaQuery(ExpertSensitiveInfoModifyDetailRecord.class) | |||
@@ -685,7 +661,7 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
if (StringUtils.isNotEmpty(modifyJson)) { | |||
modifyJson = GzipUtil.unzip(modifyJson); | |||
} | |||
List<SensitiveModifySegment> sensitiveModifySegments = JSONUtils.parseArray(modifyJson, SensitiveModifySegment.class); | |||
List<SensitiveModifySegment> sensitiveModifySegments = JSONUtil.toList(modifyJson, SensitiveModifySegment.class); | |||
Map<ExpertUserInfoSensitiveFieldEnum, SensitiveModifySegment> collectMap = sensitiveModifySegments | |||
.stream().collect(Collectors.toMap(SensitiveModifySegment::getFieldName, Function.identity())); | |||
@@ -720,7 +696,7 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
case title_level: { | |||
SensitiveModifySegment segment = collectMap.get(sensitiveFieldEnum); | |||
if (Objects.nonNull(segment)) { | |||
List<DictFieldInfoDTO> dictionaryFieldInfoList = SensitiveModifySegmentUtils.getDictionaryFieldInfoListApply(segment); | |||
List<DictFieldInfoDTO> dictionaryFieldInfoList = SensitiveModifySegmentUtil.getDictionaryFieldInfoListApply(segment); | |||
updateExpertDictionaryList(expertUserId, dictionaryFieldInfoList, sensitiveFieldEnum.getKey()); | |||
} | |||
} | |||
@@ -742,7 +718,7 @@ public class ExpertInfoServiceImpl implements ExpertInfoService { | |||
case other: { | |||
SensitiveModifySegment segment = collectMap.get(sensitiveFieldEnum); | |||
if (Objects.nonNull(segment)) { | |||
List<TagFieldInfo> tagFieldInfoList = SensitiveModifySegmentUtils.getTagFieldInfoListApply(segment); | |||
List<TagFieldInfo> tagFieldInfoList = SensitiveModifySegmentUtil.getTagFieldInfoListApply(segment); | |||
updateExpertTagList(expertUserId, tagFieldInfoList, sensitiveFieldEnum.getKey()); | |||
} | |||
} | |||
@@ -1,11 +1,11 @@ | |||
package com.hz.pm.api.expert.utils; | |||
import com.ningdatech.basic.exception.BizException; | |||
import cn.hutool.json.JSONUtil; | |||
import com.hz.pm.api.expert.model.DictFieldInfoDTO; | |||
import com.hz.pm.api.expert.model.SensitiveModifySegment; | |||
import com.hz.pm.api.expert.model.TagFieldInfo; | |||
import com.ningdatech.yxt.utils.JSONUtils; | |||
import com.ningdatech.basic.exception.BizException; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
@@ -16,9 +16,9 @@ import java.util.Objects; | |||
* @date 2022/8/24 下午3:36 | |||
*/ | |||
public class SensitiveModifySegmentUtils { | |||
public class SensitiveModifySegmentUtil { | |||
private SensitiveModifySegmentUtils() { | |||
private SensitiveModifySegmentUtil() { | |||
} | |||
public static List<TagFieldInfo> getTagFieldInfoListApply(SensitiveModifySegment segment) { | |||
@@ -28,7 +28,7 @@ public class SensitiveModifySegmentUtils { | |||
case industry_sector: | |||
case other: | |||
if (Objects.nonNull(segment.getApply())) { | |||
return JSONUtils.parseArray(segment.getApply().toString(), TagFieldInfo.class); | |||
return JSONUtil.toList(segment.getApply().toString(), TagFieldInfo.class); | |||
} | |||
return new ArrayList<>(); | |||
default: | |||
@@ -42,7 +42,7 @@ public class SensitiveModifySegmentUtils { | |||
case administrative_rank: | |||
case title_level: { | |||
if (Objects.nonNull(segment.getApply())) { | |||
return JSONUtils.parseArray(segment.getApply().toString(), DictFieldInfoDTO.class); | |||
return JSONUtil.toList(segment.getApply().toString(), DictFieldInfoDTO.class); | |||
} | |||
return new ArrayList<>(); | |||
} |
@@ -88,7 +88,7 @@ public class MhFileClient { | |||
} | |||
ClassPathResource resource = new ClassPathResource("/response/ret-xcfhx-report-file.pdf"); | |||
File destFile = new File(FileUtil.getTmpDirPath() + RandomUtil.randomString(20) + ".pdf"); | |||
return FileUtil.copy(resource.getFile(), destFile, true); | |||
return FileUtil.writeFromStream(resource.getStream(), destFile, true); | |||
} | |||
} | |||
@@ -0,0 +1,105 @@ | |||
package com.hz.pm.api.external; | |||
import cn.hutool.core.lang.Assert; | |||
import cn.hutool.core.lang.TypeReference; | |||
import cn.hutool.extra.spring.SpringUtil; | |||
import cn.hutool.http.HttpUtil; | |||
import cn.hutool.json.JSONObject; | |||
import cn.hutool.json.JSONUtil; | |||
import com.hz.pm.api.external.model.dto.CallRetDTO; | |||
import lombok.Data; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.core.env.Environment; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.List; | |||
/** | |||
* <p> | |||
* MobileCallClient | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 17:20 2024/4/25 | |||
*/ | |||
@Slf4j | |||
public class MobileCallClient { | |||
@Data | |||
public static class SendCallDTO { | |||
private String requestId; | |||
} | |||
@Data | |||
public static class ReplyCallDTO { | |||
private String remoteNumber; | |||
private String dtmf; | |||
private Date sendDate; | |||
} | |||
//================================================================================================================== | |||
private static final Object LOCK = new Object(); | |||
private static final String CALL_HOST_KEY = "mobile-call.host"; | |||
private static final String CALL_OPEN_KEY = "mobile-call.open"; | |||
//================================================================================================================== | |||
private static volatile MobileCallClient client; | |||
private static volatile boolean mobileCallOpen = Boolean.TRUE; | |||
public static MobileCallClient getClient() { | |||
if (mobileCallOpen && client == null) { | |||
synchronized (LOCK) { | |||
if (client == null) { | |||
Environment environment = SpringUtil.getBean(Environment.class); | |||
mobileCallOpen = environment.getProperty(CALL_OPEN_KEY, Boolean.class, Boolean.TRUE); | |||
if (mobileCallOpen) { | |||
String callHost = environment.getProperty(CALL_HOST_KEY); | |||
Assert.notBlank(callHost, "语音服务地址不能为空"); | |||
client = new MobileCallClient(callHost); | |||
} | |||
} | |||
} | |||
} | |||
Assert.isTrue(mobileCallOpen, "语音服务未开启"); | |||
return client; | |||
} | |||
private final String callHost; | |||
private MobileCallClient(String callHost) { | |||
this.callHost = callHost; | |||
} | |||
private static final String MOBILE_CALL_URL = "/send_batch_voice_sms"; | |||
private static final String MOBILE_REPLY_URL = "/query_voice_sms"; | |||
public CallRetDTO<SendCallDTO> sendCall(Collection<String> phones, String content) { | |||
String url = callHost + MOBILE_CALL_URL; | |||
JSONObject bodyObj = new JSONObject(); | |||
bodyObj.set("remote_numbers", phones); | |||
bodyObj.set("content", content); | |||
String retStr = HttpUtil.post(url, bodyObj.toString()); | |||
return JSONUtil.toBean(retStr, new TypeReference<CallRetDTO<SendCallDTO>>() { | |||
}, false); | |||
} | |||
public CallRetDTO<List<ReplyCallDTO>> replyCall(String requestId) { | |||
String url = callHost + MOBILE_REPLY_URL + "?request_id=" + requestId; | |||
String retStr = HttpUtil.get(url); | |||
return JSONUtil.toBean(retStr, new TypeReference<CallRetDTO<List<ReplyCallDTO>>>() { | |||
}, false); | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
package com.hz.pm.api.external.model.dto; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* CallRetDTO | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 17:38 2024/4/25 | |||
*/ | |||
@Data | |||
public class CallRetDTO<T> { | |||
private Boolean success; | |||
private T data; | |||
public boolean isOk() { | |||
return Boolean.TRUE.equals(success); | |||
} | |||
} |
@@ -0,0 +1,147 @@ | |||
package com.hz.pm.api.external.sms; | |||
import cn.hutool.core.lang.Assert; | |||
import cn.hutool.core.lang.TypeReference; | |||
import cn.hutool.extra.spring.SpringUtil; | |||
import cn.hutool.http.HttpUtil; | |||
import cn.hutool.json.JSONUtil; | |||
import com.alibaba.fastjson.JSON; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsReceipt; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsSendDTO; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.core.env.Environment; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
/** | |||
* <p> | |||
* MhSmsClient | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 16:38 2024/4/25 | |||
*/ | |||
@Slf4j | |||
public class MhSmsClient { | |||
private static volatile MhSmsClient client; | |||
private static volatile boolean smsSendOpen = Boolean.TRUE; | |||
private static final String SMS_HOST_KEY = "sms-client.host"; | |||
private static final String SMS_OPEN_KEY = "sms-client.open"; | |||
private static final Object LOCK = new Object(); | |||
public static MhSmsClient getClient() { | |||
if (smsSendOpen && client == null) { | |||
synchronized (LOCK) { | |||
if (client == null) { | |||
Environment environment = SpringUtil.getBean(Environment.class); | |||
smsSendOpen = environment.getProperty(SMS_OPEN_KEY, Boolean.class, Boolean.TRUE); | |||
if (smsSendOpen) { | |||
String smsHost = environment.getProperty(SMS_HOST_KEY); | |||
Assert.notBlank(smsHost, "短信服务地址不能为空"); | |||
client = new MhSmsClient(smsHost); | |||
} | |||
} | |||
} | |||
} | |||
Assert.isTrue(smsSendOpen, "短信服务未开启"); | |||
return client; | |||
} | |||
//================================================================================================================== | |||
private MhSmsClient(String smsUrl) { | |||
this.smsUrl = smsUrl; | |||
} | |||
/** | |||
* 短信服务地址前缀 | |||
*/ | |||
private final String smsUrl; | |||
/** | |||
* 获取短信回复结果 | |||
*/ | |||
private static final String SMS_REPLY = "/sms/reply"; | |||
/** | |||
* 短信发送 | |||
*/ | |||
private static final String SMS_SEND = "/sms/send"; | |||
/** | |||
* 短信状态 | |||
*/ | |||
private static final String SMS_RECEIPT = "/sms/status"; | |||
/** | |||
* 短信回复 | |||
* | |||
* @param result \ | |||
* @return \ | |||
*/ | |||
public SmsReplyDTO smsReply(String result) { | |||
String refreshUrl = smsUrl + SMS_REPLY; | |||
Map<String, Object> map = new HashMap<>(); | |||
map.put("result", result); | |||
String responseResult = HttpUtil.post(refreshUrl, map); | |||
JSONObject responseJson = JSON.parseObject(responseResult, JSONObject.class); | |||
String fileData = responseJson.getString("data"); | |||
return JSONUtil.toBean(fileData, new TypeReference<SmsReplyDTO>() { | |||
}, false); | |||
} | |||
/** | |||
* 短信发送 | |||
* | |||
* @return \ | |||
*/ | |||
public SmsDTO<SmsSendDTO> smsSend(String content, Collection<String> phones) { | |||
String phonesSplit = String.join(",", phones); | |||
String refreshUrl = smsUrl + SMS_SEND; | |||
Map<String, Object> map = new HashMap<>(); | |||
map.put("content", content); | |||
map.put("phones", phonesSplit); | |||
String responseResult = HttpUtil.post(refreshUrl, JSON.toJSONString(map)); | |||
return JSONUtil.toBean(responseResult, new TypeReference<SmsDTO<SmsSendDTO>>() { | |||
}, false); | |||
} | |||
/** | |||
* 短信发送 | |||
* | |||
* @return \ | |||
*/ | |||
public SmsDTO<SmsSendDTO> smsSend(String content, String phone) { | |||
return smsSend(content, Collections.singletonList(phone)); | |||
} | |||
/** | |||
* 短信回执 | |||
* | |||
* @param result \ | |||
* @return \ | |||
*/ | |||
public SmsReceipt smsReceipt(String result) { | |||
String refreshUrl = smsUrl + SMS_RECEIPT; | |||
HashMap<String, Object> map = new HashMap<>(); | |||
map.put("result", result); | |||
String responseResult = HttpUtil.post(refreshUrl, map); | |||
JSONObject responseJson = JSON.parseObject(responseResult, JSONObject.class); | |||
String fileData = responseJson.getString("data"); | |||
JSONObject jsonObject = JSON.parseObject(fileData); | |||
Map<String, Object> dataMap = jsonObject.getInnerMap(); | |||
if (dataMap.get("result") instanceof Boolean) { | |||
return SmsReceipt.builder().error(dataMap.get("error").toString()).build(); | |||
} | |||
return JSONObject.parseObject(fileData, SmsReceipt.class); | |||
} | |||
} |
@@ -1,86 +0,0 @@ | |||
package com.hz.pm.api.external.sms; | |||
import cn.hutool.core.lang.TypeReference; | |||
import cn.hutool.http.HttpUtil; | |||
import cn.hutool.json.JSONUtil; | |||
import com.alibaba.fastjson.JSON; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsReceipt; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyResponse; | |||
import com.hz.pm.api.external.sms.vo.SmsSendResponse; | |||
import lombok.RequiredArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Value; | |||
import org.springframework.stereotype.Component; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
@Slf4j | |||
@Component | |||
@RequiredArgsConstructor | |||
public class SmsServiceClient { | |||
/** | |||
* 短信服务 | |||
*/ | |||
@Value("${sms.client-url:}") | |||
private String smsUrl; | |||
public static final String SMS_REPLY = "/sms/reply"; | |||
public static final String SMS_SEND = "/sms/send"; | |||
public static final String SMS_RECEIPT = "/sms/status"; | |||
/** | |||
* 短信回复 | |||
* @param result | |||
* @return | |||
*/ | |||
public SmsReplyResponse smsReply(String result) { | |||
String refreshUrl = smsUrl + SMS_REPLY; | |||
HashMap<String,Object> map = new HashMap<>(); | |||
map.put("result",result); | |||
String responseResult = HttpUtil.post(refreshUrl, map); | |||
JSONObject responseJson = JSON.parseObject(responseResult, JSONObject.class); | |||
String fileData = responseJson.getString("data"); | |||
return JSONUtil.toBean(fileData, new TypeReference<SmsReplyResponse>() { | |||
}, false); | |||
} | |||
/** | |||
* 短信发送 | |||
* @return | |||
*/ | |||
public SmsDTO<SmsSendResponse> smsSend(String content, List<String> phones) { | |||
String phonesSplit = String.join(",", phones); | |||
String refreshUrl = smsUrl + SMS_SEND; | |||
Map<String,Object> map = new HashMap<>(); | |||
map.put("content",content); | |||
map.put("phones",phonesSplit); | |||
String responseResult = HttpUtil.post(refreshUrl, JSON.toJSONString(map)); | |||
return JSONUtil.toBean(responseResult, new TypeReference<SmsDTO<SmsSendResponse>>() { | |||
}, false); | |||
} | |||
/** | |||
* 短信回执 | |||
* @param result | |||
* @return | |||
*/ | |||
public SmsReceipt smsReceipt(String result) { | |||
String refreshUrl = smsUrl + SMS_RECEIPT; | |||
HashMap<String,Object> map = new HashMap<>(); | |||
map.put("result",result); | |||
String responseResult = HttpUtil.post(refreshUrl, map); | |||
JSONObject responseJson = JSON.parseObject(responseResult, JSONObject.class); | |||
String fileData = responseJson.getString("data"); | |||
JSONObject jsonObject = JSON.parseObject(fileData); | |||
Map<String, Object> dataMap = jsonObject.getInnerMap(); | |||
if(dataMap.get("result") instanceof Boolean){ | |||
return SmsReceipt.builder().error(dataMap.get("error").toString()).build(); | |||
} | |||
return JSONObject.parseObject(fileData, SmsReceipt.class); | |||
} | |||
} |
@@ -2,13 +2,21 @@ package com.hz.pm.api.external.sms.dto; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* SmsDTO | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 15:56 2024/4/25 | |||
*/ | |||
@Data | |||
public class SmsDTO<T> { | |||
/** | |||
* 成功状态码 | |||
*/ | |||
private static final int OK_CODE = 200; | |||
private static final Integer OK_CODE = 200; | |||
/** | |||
* 失败状态码(未找到资源) | |||
*/ | |||
@@ -19,4 +27,9 @@ public class SmsDTO<T> { | |||
private String msg; | |||
private T data; | |||
public boolean isOk() { | |||
return OK_CODE.equals(this.getCode()); | |||
} | |||
} |
@@ -6,9 +6,17 @@ import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* <p> | |||
* SmsReplyDTO | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 17:05 2024/4/25 | |||
*/ | |||
@Data | |||
@ApiModel("短信回复响应内容") | |||
public class SmsReplyResponse { | |||
public class SmsReplyDTO { | |||
@ApiModelProperty("参数校验失败的错误提示") | |||
private String error; |
@@ -2,8 +2,16 @@ package com.hz.pm.api.external.sms.vo; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* SmsSendDTO | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 16:56 2024/4/25 | |||
*/ | |||
@Data | |||
public class SmsSendResponse { | |||
public class SmsSendDTO { | |||
//发送失败的手机号 | |||
private String errorPhone; |
@@ -32,7 +32,7 @@ import com.hz.pm.api.meeting.entity.domain.MeetingExpert; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertInviteTypeEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.MeetingStatusEnum; | |||
import com.hz.pm.api.meeting.helper.MeetingCallOrMsgHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingNotifyHelper; | |||
import com.hz.pm.api.meeting.service.IMeetingExpertService; | |||
import com.hz.pm.api.meeting.service.IMeetingService; | |||
import com.hz.pm.api.meeting.task.ExpertRandomInviteTask; | |||
@@ -75,7 +75,7 @@ public class LeaveManage { | |||
private final IExpertLeaveDetailService leaveDetailService; | |||
private final IExpertMetaApplyService metaApplyService; | |||
private final FileService fileService; | |||
private final MeetingCallOrMsgHelper meetingCallOrMsgHelper; | |||
private final MeetingNotifyHelper meetingNotifyHelper; | |||
private static final int HOURS_BEFORE_MEETING = 2; | |||
@@ -202,7 +202,7 @@ public class LeaveManage { | |||
.eq(Meeting::getId, meeting.getId()); | |||
meetingService.update(mUpdate); | |||
} | |||
meetingCallOrMsgHelper.sendExpertLeaveMsg(expert, meeting); | |||
meetingNotifyHelper.sendExpertLeaveMsg(expert, meeting); | |||
// 临时请假无需审核 | |||
leave.setAuditId(0L); | |||
leave.setStatus(LeaveStatusEnum.PASSED.getCode()); | |||
@@ -1,10 +1,10 @@ | |||
package com.hz.pm.api.meeting.controller; | |||
import com.hz.pm.api.external.sms.SmsServiceClient; | |||
import com.hz.pm.api.external.sms.MhSmsClient; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsReceipt; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyResponse; | |||
import com.hz.pm.api.external.sms.vo.SmsSendResponse; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsSendDTO; | |||
import com.hz.pm.api.meeting.entity.req.MeetingCalenderReq; | |||
import com.hz.pm.api.meeting.entity.vo.*; | |||
import com.hz.pm.api.meeting.manage.DashboardManage; | |||
@@ -39,24 +39,22 @@ public class ExpertDashboardController { | |||
private final DashboardManage dashboardManage; | |||
private final SmsServiceClient smsServiceClient; | |||
@ApiOperation("短信发送") | |||
@GetMapping("/sendSms") | |||
public SmsDTO<SmsSendResponse> test(String phone) { | |||
return smsServiceClient.smsSend(VoiceSmsTemplateConst.EXPERT_INVITE, Collections.singletonList(phone)); | |||
public SmsDTO<SmsSendDTO> test(String phone) { | |||
return MhSmsClient.getClient().smsSend(VoiceSmsTemplateConst.EXPERT_INVITE, Collections.singletonList(phone)); | |||
} | |||
@ApiOperation("短信回复") | |||
@GetMapping("/aplySms") | |||
public SmsReplyResponse smsReply(String uuid) { | |||
return smsServiceClient.smsReply(uuid); | |||
public SmsReplyDTO smsReply(String uuid) { | |||
return MhSmsClient.getClient().smsReply(uuid); | |||
} | |||
@ApiOperation("短信回执") | |||
@GetMapping("/status") | |||
public SmsReceipt smsReceipt(String uuid) { | |||
return smsServiceClient.smsReceipt(uuid); | |||
return MhSmsClient.getClient().smsReceipt(uuid); | |||
} | |||
@ApiOperation("会议日历") | |||
@@ -26,6 +26,7 @@ public class MeetingExpert implements Serializable { | |||
@Tolerate | |||
public MeetingExpert() { | |||
// 默认无参构造 | |||
} | |||
private static final long serialVersionUID = 1L; | |||
@@ -61,11 +62,9 @@ public class MeetingExpert implements Serializable { | |||
@ApiModelProperty("是否已确认名单") | |||
private Boolean confirmedRoster; | |||
@ApiModelProperty("提交标识") | |||
private String submitKey; | |||
@ApiModelProperty("短信发送返回的uuid") | |||
private String smsUuid; | |||
@ApiModelProperty("通知方式:1电话 2短信") | |||
private String notifyWay; | |||
@@ -3,37 +3,34 @@ package com.hz.pm.api.meeting.helper; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.alibaba.fastjson.JSON; | |||
import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |||
import com.hz.pm.api.common.helper.MsgCallHelper; | |||
import com.hz.pm.api.meeting.constant.MeetingMsgTemplateConst; | |||
import com.hz.pm.api.meeting.entity.domain.Meeting; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpert; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertInviteTypeEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertNotifyTypeEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.MeetingReviewTypeEnum; | |||
import com.hz.pm.api.meeting.task.ExpertCallResultRewriteTask; | |||
import com.hz.pm.api.organization.model.entity.DingEmployeeInfo; | |||
import com.hz.pm.api.organization.model.entity.DingOrganization; | |||
import com.hz.pm.api.organization.service.IDingEmployeeInfoService; | |||
import com.hz.pm.api.organization.service.IDingOrganizationService; | |||
import com.hz.pm.api.sms.constant.VoiceSmsTemplateConst; | |||
import com.hz.pm.api.sms.utils.DateUtil; | |||
import com.hz.pm.api.staging.enums.MsgTypeEnum; | |||
import com.hz.pm.api.staging.service.INdWorkNoticeStagingService; | |||
import com.hz.pm.api.sys.model.entity.Notify; | |||
import com.hz.pm.api.sys.model.enumeration.BizTypeEnum; | |||
import com.hz.pm.api.sys.service.INotifyService; | |||
import com.hz.pm.api.todocenter.bean.entity.WorkNoticeInfo; | |||
import com.hz.pm.api.user.model.entity.UserInfo; | |||
import com.hz.pm.api.user.service.IUserInfoService; | |||
import com.ningdatech.basic.util.CollUtils; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import lombok.AllArgsConstructor; | |||
import org.springframework.stereotype.Component; | |||
import org.springframework.transaction.annotation.Transactional; | |||
import java.time.LocalDateTime; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.stream.Collectors; | |||
import java.util.*; | |||
/** | |||
* <p> | |||
@@ -45,15 +42,14 @@ import java.util.stream.Collectors; | |||
**/ | |||
@Component | |||
@AllArgsConstructor | |||
public class MeetingCallOrMsgHelper { | |||
public class MeetingNotifyHelper { | |||
private final IUserInfoService userInfoService; | |||
private final YxtClientHelper yxtClientHelper; | |||
private final MsgCallHelper msgCallHelper; | |||
private final INdWorkNoticeStagingService workNoticeStagingService; | |||
private final IDingEmployeeInfoService dingEmployeeInfoService; | |||
private final IDingOrganizationService dingOrganizationService; | |||
private final INotifyService notifyService; | |||
private final ExpertCallResultRewriteTask expertCallResultRewriteTask; | |||
private static String officialTime(LocalDateTime time) { | |||
return time.format(DateUtil.DTF_YMD_HM); | |||
@@ -100,10 +96,7 @@ public class MeetingCallOrMsgHelper { | |||
UserInfo info = userInfoService.getById(userId); | |||
String msgContent = String.format(MeetingMsgTemplateConst.INVITE_END, meetingName); | |||
// 音信通消息 | |||
SendSmsContext yxtContent = new SendSmsContext(); | |||
yxtContent.setContent(msgContent); | |||
yxtContent.setReceiveNumber(info.getMobile()); | |||
yxtClientHelper.sendSms(yxtContent); | |||
msgCallHelper.sendMsg(info.getMobile(), msgContent, BizTypeEnum.EXPERT_INVITE_STOP); | |||
// 发送工作通知 | |||
if (info.getAccountId() != null) { | |||
WorkNoticeInfo swn = getSendWorkNoticeInfo(info.getAccountId()); | |||
@@ -124,7 +117,6 @@ public class MeetingCallOrMsgHelper { | |||
String sTime = officialTime(meeting.getStartTime()); | |||
String eTime = officialTime(meeting.getEndTime()); | |||
String meetingTime = sTime + "至" + eTime; | |||
List<SendSmsContext> yxtContents = new ArrayList<>(); | |||
List<Notify> notifies = new ArrayList<>(); | |||
List<WorkNoticeInfo> workingNotices = new ArrayList<>(); | |||
experts.forEach(w -> { | |||
@@ -132,10 +124,7 @@ public class MeetingCallOrMsgHelper { | |||
w.getExpertName(), officialTime(w.getCreateOn()), meetingTime, meeting.getMeetingAddress(), | |||
meeting.getConnecter(), meeting.getContact()); | |||
// 音信通消息 | |||
SendSmsContext yxtContent = new SendSmsContext(); | |||
yxtContent.setContent(msgContent); | |||
yxtContent.setReceiveNumber(w.getMobile()); | |||
yxtContents.add(yxtContent); | |||
msgCallHelper.sendMsg(w.getMobile(), msgContent, BizTypeEnum.MEETING_ROSTER_CONFIRM); | |||
UserInfo info = userMap.get(w.getExpertId()); | |||
// 发送工作通知 | |||
if (info.getAccountId() != null) { | |||
@@ -149,7 +138,6 @@ public class MeetingCallOrMsgHelper { | |||
} | |||
}); | |||
notifyService.saveBatch(notifies); | |||
yxtClientHelper.sendSms(yxtContents); | |||
workNoticeStagingService.addByWorkNotice(workingNotices, MsgTypeEnum.EXPERT_REVIEW); | |||
} | |||
@@ -157,15 +145,11 @@ public class MeetingCallOrMsgHelper { | |||
public void sendCancelMeetingMsg(List<MeetingExpert> experts, Meeting meeting) { | |||
String startTime = officialTime(meeting.getStartTime()); | |||
String meetingType = MeetingReviewTypeEnum.getValue(meeting.getType()); | |||
List<SendSmsContext> contexts = experts.stream().map(w -> { | |||
for (MeetingExpert expert : experts) { | |||
String content = String.format(MeetingMsgTemplateConst.MEETING_CANCEL, | |||
w.getExpertName(), startTime, meetingType, meeting.getContact()); | |||
SendSmsContext context = new SendSmsContext(); | |||
context.setReceiveNumber(w.getMobile()); | |||
context.setContent(content); | |||
return context; | |||
}).collect(Collectors.toList()); | |||
yxtClientHelper.sendSms(contexts); | |||
expert.getExpertName(), startTime, meetingType, meeting.getContact()); | |||
msgCallHelper.sendMsg(expert.getMobile(), content, BizTypeEnum.MEETING_CANCEL); | |||
} | |||
} | |||
/** | |||
@@ -175,27 +159,36 @@ public class MeetingCallOrMsgHelper { | |||
* @param experts 待通知专家 | |||
* @author WendyYang | |||
**/ | |||
// public void callExpertByMeeting(Meeting meeting, List<MeetingExpert> experts) { | |||
// String voiceContent = String.format(VoiceSmsTemplateConst.EXPERT_INVITE, | |||
// meeting.getHoldOrg(), meeting.getName(), officialTime(meeting.getStartTime()), | |||
// meeting.getMeetingAddress()); | |||
// List<SubmitTaskCallContext> callContexts = CollUtils.convert(experts, w -> { | |||
// SubmitTaskCallContext context = new SubmitTaskCallContext(); | |||
// context.setContent(voiceContent); | |||
// context.setReceiveNumber(w.getMobile()); | |||
// return context; | |||
// }); | |||
// String submitKey = yxtClientHelper.submitCallTask(callContexts); | |||
// experts.forEach(w -> w.setSubmitKey(submitKey)); | |||
// } | |||
public void smsOrCallExpertByMeeting(Meeting meeting, List<MeetingExpert> experts) { | |||
if(ExpertNotifyTypeEnum.CALL.eq(meeting.getNotifyWay())){ | |||
// todo 电话通知 | |||
}else if(ExpertNotifyTypeEnum.SMS.eq(meeting.getNotifyWay())){ | |||
// 短信通知 | |||
String smsUuid = expertCallResultRewriteTask.sendExpertSms(experts, meeting); | |||
experts.forEach(w -> w.setSmsUuid(smsUuid)); | |||
private void callExperts(Meeting meeting, List<MeetingExpert> experts) { | |||
String content = String.format(VoiceSmsTemplateConst.EXPERT_INVITE, | |||
meeting.getHoldOrg(), meeting.getName(), officialTime(meeting.getStartTime()), | |||
meeting.getMeetingAddress()); | |||
Set<String> phones = CollUtils.fieldSet(experts, MeetingExpert::getMobile); | |||
String submitKey = msgCallHelper.sendCall(phones, content, BizTypeEnum.EXPERT_INVITE); | |||
experts.forEach(w -> w.setSubmitKey(submitKey)); | |||
} | |||
/** | |||
* 发送专家短信任务 | |||
* | |||
* @param experts 专家 | |||
* @param meeting 会议 | |||
*/ | |||
private void smsExperts(Meeting meeting, List<MeetingExpert> experts) { | |||
String content = String.format(VoiceSmsTemplateConst.EXPERT_INVITE, | |||
meeting.getHoldOrg(), meeting.getName(), officialTime(meeting.getStartTime()), | |||
meeting.getMeetingAddress()); | |||
Set<String> phones = CollUtils.fieldSet(experts, MeetingExpert::getMobile); | |||
String submitKey = msgCallHelper.sendMsg(phones, content, BizTypeEnum.EXPERT_INVITE); | |||
// 短信发送成功返回的UUID | |||
experts.forEach(w -> w.setSubmitKey(submitKey)); | |||
} | |||
public void notifyExperts(Meeting meeting, List<MeetingExpert> experts) { | |||
if (ExpertNotifyTypeEnum.CALL.eq(meeting.getNotifyWay())) { | |||
callExperts(meeting, experts); | |||
} else if (ExpertNotifyTypeEnum.SMS.eq(meeting.getNotifyWay())) { | |||
smsExperts(meeting, experts); | |||
} | |||
} | |||
@@ -209,10 +202,7 @@ public class MeetingCallOrMsgHelper { | |||
msgContent = String.format(MeetingMsgTemplateConst.EXPERT_LEAVE_APPOINT, meeting.getName(), expert.getExpertName()); | |||
} | |||
UserInfo info = userInfoService.getById(userId); | |||
SendSmsContext yxtContent = new SendSmsContext(); | |||
yxtContent.setContent(msgContent); | |||
yxtContent.setReceiveNumber(info.getMobile()); | |||
yxtClientHelper.sendSms(yxtContent); | |||
msgCallHelper.sendMsg(info.getMobile(), msgContent, BizTypeEnum.EXPERT_LEAVE); | |||
// 发送工作通知 | |||
if (info.getAccountId() != null) { | |||
WorkNoticeInfo swn = getSendWorkNoticeInfo(info.getAccountId()); |
@@ -1,69 +0,0 @@ | |||
package com.hz.pm.api.meeting.helper; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.hz.pm.api.external.sms.SmsServiceClient; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsSendResponse; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpertSms; | |||
import com.hz.pm.api.meeting.mapper.MeetingExpertSmsMapper; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd; | |||
import com.ningdatech.yxt.model.cmd.SubmitTaskCallCmd; | |||
import com.ningdatech.yxt.model.cmd.SubmitTaskCallResponse; | |||
import com.ningdatech.yxt.model.response.SendSmsResponse; | |||
import lombok.AllArgsConstructor; | |||
import org.springframework.context.annotation.Primary; | |||
import org.springframework.stereotype.Component; | |||
import java.time.LocalDateTime; | |||
import java.util.Arrays; | |||
/** | |||
* @author wangrenkang | |||
* @date 2024-02-22 16:39:35 | |||
*/ | |||
@Component | |||
@AllArgsConstructor | |||
@Primary | |||
public class SmsOrCallClient implements com.ningdatech.yxt.client.YxtClient { | |||
private final SmsServiceClient smsServiceClient; | |||
private final MeetingExpertSmsMapper meetingExpertSmsMapper; | |||
/** | |||
* 发送短信 | |||
* @param sendSmsCmd 短信内容,短信手机号 | |||
* @return | |||
*/ | |||
@Override | |||
public SendSmsResponse submitSmsTask(SendSmsCmd sendSmsCmd) { | |||
sendSmsCmd.getContextList().forEach(sms ->{ | |||
SmsDTO<SmsSendResponse> smsSendResponseSmsDto = smsServiceClient.smsSend(sms.getContent(), Arrays.asList(sms.getReceiveNumber())); | |||
String resultUuid = smsSendResponseSmsDto.getData().getResult(); | |||
// 保存短信发送记录 | |||
MeetingExpertSms meetingExpertSms = MeetingExpertSms.builder() | |||
.createOn(LocalDateTime.now()) | |||
.smsUuid(resultUuid) | |||
.content(sms.getContent()) | |||
.phones(sms.getReceiveNumber()) | |||
.smsResult(smsSendResponseSmsDto.toString()) | |||
.build(); | |||
meetingExpertSmsMapper.insert(meetingExpertSms); | |||
}); | |||
return null; | |||
} | |||
@Override | |||
public SubmitTaskCallResponse submitTaskCall(SubmitTaskCallCmd submitTaskCallCmd) { | |||
return null; | |||
} | |||
@Override | |||
public JSONObject getSentResultSms(String transactionId) { | |||
return null; | |||
} | |||
@Override | |||
public JSONObject getSentResultCall(String transactionId) { | |||
return null; | |||
} | |||
} |
@@ -1,46 +0,0 @@ | |||
package com.hz.pm.api.meeting.helper; | |||
import com.ningdatech.yxt.client.YxtClient; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import com.ningdatech.yxt.model.cmd.SubmitTaskCallResponse; | |||
import lombok.AllArgsConstructor; | |||
import org.springframework.stereotype.Component; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import static com.ningdatech.yxt.model.cmd.SubmitTaskCallCmd.SubmitTaskCallContext; | |||
import static com.ningdatech.yxt.model.cmd.SubmitTaskCallCmd.of; | |||
/** | |||
* <p> | |||
* YxtCallOrSmsHelper | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 23:59 2022/8/31 | |||
*/ | |||
@Component | |||
@AllArgsConstructor | |||
public class YxtClientHelper { | |||
private final YxtClient yxtClient; | |||
public String submitCallTask(List<SubmitTaskCallContext> callContexts) { | |||
SubmitTaskCallResponse callResponse = yxtClient.submitTaskCall(of(callContexts)); | |||
return callResponse.getSubmitKey(); | |||
} | |||
public void sendSms(List<SendSmsContext> smsList) { | |||
SendSmsCmd cmd = new SendSmsCmd(); | |||
cmd.setContextList(smsList); | |||
// cmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
yxtClient.submitSmsTask(cmd); | |||
} | |||
public void sendSms(SendSmsContext sms) { | |||
sendSms(Collections.singletonList(sms)); | |||
} | |||
} |
@@ -20,7 +20,7 @@ import com.hz.pm.api.meeting.entity.enumeration.AvoidTypeEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.MeetingStatusEnum; | |||
import com.hz.pm.api.meeting.helper.ExpertInviteHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingCallOrMsgHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingNotifyHelper; | |||
import com.hz.pm.api.meeting.service.IExpertInviteRuleService; | |||
import com.hz.pm.api.meeting.service.IMeetingExpertService; | |||
import com.hz.pm.api.meeting.service.IMeetingService; | |||
@@ -68,7 +68,7 @@ public class ExpertInviteManage { | |||
private final IExpertUserFullInfoService expertUserFullInfoService; | |||
private final IMeetingService meetingService; | |||
private final IExpertAvoidCompanyService expertAvoidCompanyService; | |||
private final MeetingCallOrMsgHelper meetingCallOrMsgHelper; | |||
private final MeetingNotifyHelper meetingNotifyHelper; | |||
private final IExpertGovBusinessStripService expertGovBusinessStripService; | |||
@Value("#{randomInviteProperties.recentMeetingCount}") | |||
@@ -576,9 +576,7 @@ public class ExpertInviteManage { | |||
expertInserts.add(expert); | |||
}); | |||
} | |||
// meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertInserts); | |||
// 短信或电话提醒 | |||
meetingCallOrMsgHelper.smsOrCallExpertByMeeting(meeting, expertInserts); | |||
meetingNotifyHelper.notifyExperts(meeting, expertInserts); | |||
} | |||
meetingExpertService.saveBatch(expertInserts); | |||
} | |||
@@ -29,7 +29,7 @@ import com.hz.pm.api.meeting.entity.enumeration.MeetingStatusEnum; | |||
import com.hz.pm.api.meeting.entity.req.*; | |||
import com.hz.pm.api.meeting.entity.vo.*; | |||
import com.hz.pm.api.meeting.helper.ExpertInviteHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingCallOrMsgHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingNotifyHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingManageHelper; | |||
import com.hz.pm.api.meeting.service.*; | |||
import com.hz.pm.api.meeting.task.ExpertRandomInviteTask; | |||
@@ -91,7 +91,7 @@ public class MeetingManage { | |||
private final IBelongOrgService belongOrgService; | |||
private final IDingOrganizationService dingOrganizationService; | |||
private final ExpertInviteHelper expertInviteHelper; | |||
private final MeetingCallOrMsgHelper meetingCallOrMsgHelper; | |||
private final MeetingNotifyHelper meetingNotifyHelper; | |||
private final IMeetingExpertJudgeService expertJudgeService; | |||
private final IMeetingExpertJudgeService meetingExpertJudgeService; | |||
@@ -686,7 +686,7 @@ public class MeetingManage { | |||
// 发送通知给专家 | |||
List<MeetingExpert> experts = meetingExpertService.listAgreedExperts(meetingId); | |||
if (!experts.isEmpty()) { | |||
meetingCallOrMsgHelper.sendCancelMeetingMsg(experts, meeting); | |||
meetingNotifyHelper.sendCancelMeetingMsg(experts, meeting); | |||
} | |||
} finally { | |||
distributedLock.releaseLock(key); | |||
@@ -846,7 +846,7 @@ public class MeetingManage { | |||
.in(MeetingExpert::getId, currConfirmedMeIds) | |||
.set(MeetingExpert::getConfirmedRoster, Boolean.TRUE); | |||
meetingExpertService.update(meUpdate); | |||
meetingCallOrMsgHelper.sendConfirmedRosterMsg(expertNoticing, meeting); | |||
meetingNotifyHelper.sendConfirmedRosterMsg(expertNoticing, meeting); | |||
} finally { | |||
distributedLock.releaseLock(key); | |||
} | |||
@@ -1,47 +1,33 @@ | |||
package com.hz.pm.api.meeting.task; | |||
import cn.hutool.core.util.ObjectUtil; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |||
import cn.hutool.core.util.StrUtil; | |||
import cn.hutool.json.JSONUtil; | |||
import com.baomidou.mybatisplus.core.conditions.Wrapper; | |||
import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |||
import com.hz.pm.api.common.util.StrUtils; | |||
import com.hz.pm.api.external.sms.SmsServiceClient; | |||
import com.hz.pm.api.external.sms.dto.SmsDTO; | |||
import com.hz.pm.api.external.sms.vo.SmsReply; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyResponse; | |||
import com.hz.pm.api.external.sms.vo.SmsSendResponse; | |||
import com.hz.pm.api.common.util.ThreadPoolUtil; | |||
import com.hz.pm.api.meeting.entity.domain.ExpertInviteRule; | |||
import com.hz.pm.api.meeting.entity.domain.Meeting; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpert; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpertSms; | |||
import com.hz.pm.api.meeting.entity.dto.RandomInviteRuleDTO; | |||
import com.hz.pm.api.meeting.entity.dto.SmsReplyDetails; | |||
import com.hz.pm.api.meeting.entity.dto.YxtCallBackDTO; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertInviteTypeEnum; | |||
import com.hz.pm.api.meeting.mapper.MeetingExpertSmsMapper; | |||
import com.hz.pm.api.meeting.service.IExpertInviteRuleService; | |||
import com.hz.pm.api.meeting.service.IMeetingExpertService; | |||
import com.hz.pm.api.sms.constant.VoiceSmsTemplateConst; | |||
import com.ningdatech.basic.util.StrPool; | |||
import com.ningdatech.yxt.entity.SysMsgRecordDetail; | |||
import com.ningdatech.yxt.service.ISysMsgRecordDetailService; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord.ReplyStatus; | |||
import com.hz.pm.api.sys.service.IMsgCallRecordService; | |||
import com.ningdatech.basic.util.CollUtils; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; | |||
import org.springframework.stereotype.Component; | |||
import javax.annotation.PostConstruct; | |||
import java.time.Duration; | |||
import java.time.Instant; | |||
import java.time.LocalDateTime; | |||
import java.time.ZoneId; | |||
import java.time.format.DateTimeFormatter; | |||
import java.time.temporal.ChronoUnit; | |||
import java.util.*; | |||
import java.util.stream.Collectors; | |||
import static com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum.*; | |||
import static com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum.NOTICING; | |||
/** | |||
* <p> | |||
@@ -58,321 +44,114 @@ import static com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum.*; | |||
@AllArgsConstructor | |||
public class ExpertCallResultRewriteTask { | |||
private final RandomInviteProperties randomInviteProperties; | |||
private final ThreadPoolTaskScheduler scheduler; | |||
private final RandomInviteProperties properties; | |||
private final IMeetingExpertService meetingExpertService; | |||
private final IExpertInviteRuleService inviteRuleService; | |||
private final ISysMsgRecordDetailService msgRecordDetailService; | |||
private final SmsServiceClient smsServiceClient; | |||
private final MeetingExpertSmsMapper meetingExpertSmsMapper; | |||
private final IMsgCallRecordService msgCallRecordService; | |||
private final static int MINUTES_CALL_RESULT_FEEDBACK = 15; | |||
private static final int OVERTIME = 30; | |||
private static final String AGREE_KEY = "1"; | |||
private static final String REFUSE_KEY = "2"; | |||
@PostConstruct | |||
public void initTask() { | |||
if (!randomInviteProperties.getEnable()) { | |||
if (Boolean.FALSE.equals(properties.getEnable())) { | |||
log.warn("随机邀请已关闭……"); | |||
return; | |||
} | |||
// 处理电话结果回填 | |||
Instant startTime = Instant.now().plus(randomInviteProperties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
Duration fixedRate = Duration.ofMinutes(randomInviteProperties.getResultRewriteFixedRate()); | |||
// scheduler.scheduleAtFixedRate(this::rewritePhoneCallResult, startTime, fixedRate); | |||
Instant startTime = Instant.now().plus(properties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
Duration fixedRate = Duration.ofMinutes(properties.getResultRewriteFixedRate()); | |||
// 处理短信结果回填 | |||
Instant smsStartTime = Instant.now().plus(randomInviteProperties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
scheduler.scheduleAtFixedRate(this::expertSmsReply, smsStartTime, fixedRate); | |||
ThreadPoolUtil.SCHEDULER.scheduleAtFixedRate(this::expertReplyRewrite, startTime, fixedRate); | |||
} | |||
public void expertSmsReply() { | |||
log.info("开始执行短信回复查询任务:{}", Thread.currentThread().getName()); | |||
private void expertReplyRewrite() { | |||
log.info("开始执行短信回复查询任务"); | |||
// 查询所有邀请的专家信息 状态为通话中的 | |||
LambdaQueryWrapper<MeetingExpert> meQuery = Wrappers.lambdaQuery(MeetingExpert.class) | |||
Wrapper<MeetingExpert> meQuery = Wrappers.lambdaQuery(MeetingExpert.class) | |||
.eq(MeetingExpert::getStatus, NOTICING.getCode()) | |||
.isNotNull(MeetingExpert::getSubmitKey) | |||
.eq(MeetingExpert::getInviteType, ExpertInviteTypeEnum.RANDOM.getCode()); | |||
List<MeetingExpert> experts = meetingExpertService.list(meQuery); | |||
if (experts.isEmpty()) { | |||
log.info("暂无短信回复任务执行"); | |||
return; | |||
} | |||
// 所有随机邀请的短信Uuid | |||
List<String> smsUuids = experts.stream() | |||
.map(expert -> expert.getSmsUuid()) | |||
.filter(uuid -> uuid != null) | |||
.distinct() | |||
.collect(Collectors.toList()); | |||
Set<Long> randomRuleIds = experts.stream() | |||
.map(MeetingExpert::getRuleId).collect(Collectors.toSet()); | |||
List<String> submitKeys = CollUtils.fieldList(experts, MeetingExpert::getSubmitKey); | |||
List<String> mobiles = CollUtils.fieldList(experts, MeetingExpert::getMobile); | |||
Set<Long> ruleIds = CollUtils.fieldSet(experts, MeetingExpert::getRuleId); | |||
// 查询随机邀请回调等待时间 | |||
Map<Long, Integer> callbackMinutes = new HashMap<>(randomRuleIds.size()); | |||
if (!randomRuleIds.isEmpty()) { | |||
List<ExpertInviteRule> inviteRules = inviteRuleService.listByIds(randomRuleIds); | |||
inviteRules.forEach(expert -> { | |||
RandomInviteRuleDTO rule = JSONObject.parseObject(expert.getInviteRule(), RandomInviteRuleDTO.class); | |||
callbackMinutes.put(expert.getId(), rule.getWaitForCallbackMinutes()); | |||
}); | |||
Map<Long, Integer> callbackMinutes = new HashMap<>(ruleIds.size()); | |||
List<ExpertInviteRule> rules = inviteRuleService.listByIds(ruleIds); | |||
for (ExpertInviteRule rule : rules) { | |||
RandomInviteRuleDTO ruleDetail = JSONUtil.toBean(rule.getInviteRule(), RandomInviteRuleDTO.class); | |||
callbackMinutes.put(rule.getId(), ruleDetail.getWaitForCallbackMinutes()); | |||
} | |||
// 获取专家回复内容 | |||
SmsReplyDetails smsReplyDetails = viewSmsReplies(smsUuids); | |||
List<MeetingExpert> updates = new ArrayList<>(); | |||
// 查询短信语音回执结果 | |||
Wrapper<MsgCallRecord> mcrQuery = Wrappers.lambdaQuery(MsgCallRecord.class) | |||
.in(MsgCallRecord::getSubmitKey, submitKeys) | |||
.in(MsgCallRecord::getReceivePhone, mobiles) | |||
.isNotNull(MsgCallRecord::getReplyOn); | |||
List<MsgCallRecord> records = msgCallRecordService.list(mcrQuery); | |||
Map<String, MsgCallRecord> replayMap = CollUtils.listToMap(records, | |||
w -> w.getSubmitKey() + "#" + w.getReceivePhone()); | |||
List<MeetingExpert> changes = new ArrayList<>(); | |||
for (MeetingExpert expert : experts) { | |||
Integer minutes = ObjectUtil.defaultIfNull(callbackMinutes.get(expert.getRuleId()), MINUTES_CALL_RESULT_FEEDBACK); | |||
// 判断回复状态 | |||
Optional<Integer> status = getStatusByMsgRecordDetail(smsReplyDetails, minutes, expert); | |||
if (status.isPresent()) { | |||
MeetingExpert update = new MeetingExpert(); | |||
update.setUpdateBy(0L); | |||
update.setUpdateOn(LocalDateTime.now()); | |||
update.setId(expert.getId()); | |||
update.setStatus(status.get()); | |||
updates.add(update); | |||
} | |||
} | |||
meetingExpertService.updateBatchById(updates); | |||
} | |||
public void rewritePhoneCallResult() { | |||
log.info("开始执行电话结果回填任务:{}", Thread.currentThread().getName()); | |||
// 查询所有邀请的专家信息 状态为通话中的 | |||
LambdaQueryWrapper<MeetingExpert> meQuery = Wrappers.lambdaQuery(MeetingExpert.class) | |||
.eq(MeetingExpert::getStatus, NOTICING.getCode()) | |||
.eq(MeetingExpert::getInviteType, ExpertInviteTypeEnum.RANDOM.getCode()); | |||
List<MeetingExpert> experts = meetingExpertService.list(meQuery); | |||
if (experts.isEmpty()) { | |||
log.info("暂无电话结果回填任务执行"); | |||
return; | |||
} | |||
// 所有随机邀请的规则ID | |||
Map<Long, String> submitKeys = new HashMap<>(experts.size()); | |||
Set<Long> randomRuleIds = experts.stream().peek(w -> submitKeys.put(w.getId(), w.getSubmitKey())) | |||
.map(MeetingExpert::getRuleId).collect(Collectors.toSet()); | |||
// 查询随机邀请回调等待时间 | |||
Map<Long, Integer> callbackMinutes = new HashMap<>(randomRuleIds.size()); | |||
if (!randomRuleIds.isEmpty()) { | |||
List<ExpertInviteRule> inviteRules = inviteRuleService.listByIds(randomRuleIds); | |||
inviteRules.forEach(w -> { | |||
RandomInviteRuleDTO rule = JSONObject.parseObject(w.getInviteRule(), RandomInviteRuleDTO.class); | |||
callbackMinutes.put(w.getId(), rule.getWaitForCallbackMinutes()); | |||
String submitKeyMobile = expert.getSubmitKey() + "#" + expert.getMobile(); | |||
Integer overtime = callbackMinutes.getOrDefault(expert.getRuleId(), OVERTIME); | |||
MsgCallRecord mcr = replayMap.get(submitKeyMobile); | |||
buildStatus(mcr, overtime, expert).ifPresent(w -> { | |||
expert.setStatus(w.getCode()); | |||
changes.add(expert); | |||
}); | |||
} | |||
LambdaQueryWrapper<SysMsgRecordDetail> msgRecordDetailQuery = Wrappers.lambdaQuery(SysMsgRecordDetail.class) | |||
.in(SysMsgRecordDetail::getSubmitKey, submitKeys.values()); | |||
List<SysMsgRecordDetail> recordDetailList = msgRecordDetailService.list(msgRecordDetailQuery); | |||
if (recordDetailList.isEmpty()) { | |||
return; | |||
} | |||
Map<String, SysMsgRecordDetail> recordDetailMap = recordDetailList.stream() | |||
.collect(Collectors.toMap(w -> w.getSubmitKey() + StrPool.UNDERSCORE + w.getReceiveNumber(), w -> w)); | |||
List<MeetingExpert> updates = new ArrayList<>(), agrees = new ArrayList<>(); | |||
for (MeetingExpert expert : experts) { | |||
String key = expert.getSubmitKey() + StrPool.UNDERSCORE + expert.getMobile(); | |||
SysMsgRecordDetail msgRecordDetail = recordDetailMap.get(key); | |||
if (msgRecordDetail == null) { | |||
// 极端情况下获取不到submitKey异常情况 | |||
continue; | |||
} | |||
Integer minutes = ObjectUtil.defaultIfNull(callbackMinutes.get(expert.getRuleId()), MINUTES_CALL_RESULT_FEEDBACK); | |||
Optional<Integer> status = getStatusByMsgRecordDetail(msgRecordDetail, minutes, expert.getCreateOn()); | |||
if (status.isPresent()) { | |||
MeetingExpert update = new MeetingExpert(); | |||
update.setUpdateBy(0L); | |||
update.setUpdateOn(LocalDateTime.now()); | |||
update.setId(expert.getId()); | |||
update.setStatus(status.get()); | |||
if (AGREED.eq(update.getStatus())) { | |||
// 发送专家确认参加的短信通知 | |||
// sendAgreeNotice(expert); | |||
agrees.add(expert); | |||
} | |||
updates.add(update); | |||
} | |||
} | |||
meetingExpertService.updateBatchById(updates); | |||
meetingExpertService.updateBatchById(changes); | |||
} | |||
private static Optional<Integer> getStatusByMsgRecordDetail(SysMsgRecordDetail mrd, int minutes, LocalDateTime createOn) { | |||
LocalDateTime limitTime = LocalDateTime.now().minusMinutes(minutes); | |||
String callBackJson = mrd.getCallBackJson(); | |||
boolean waiting = limitTime.isBefore(createOn); | |||
boolean hasCallBack = StrUtils.isNotBlank(callBackJson); | |||
if (!hasCallBack && waiting) { | |||
return Optional.empty(); | |||
} | |||
/** | |||
* 构建专家状态 | |||
* | |||
* @param mcr 回复记录 | |||
* @param overtime 超时时间 | |||
* @param expert 专家 | |||
* @return 专家状态 | |||
*/ | |||
private static Optional<ExpertAttendStatusEnum> buildStatus(MsgCallRecord mcr, | |||
Integer overtime, | |||
MeetingExpert expert) { | |||
LocalDateTime now = LocalDateTime.now(); | |||
boolean isOvertime = expert.getCreateOn().plusMinutes(overtime).isBefore(now); | |||
ExpertAttendStatusEnum status; | |||
if (hasCallBack) { | |||
try { | |||
YxtCallBackDTO callback = JSONObject.parseObject(callBackJson, YxtCallBackDTO.class); | |||
LocalDateTime dialBeginTime = callback.getDialBeginTime(); | |||
if (dialBeginTime == null) { | |||
return Optional.empty(); | |||
} | |||
Integer resultCode = callback.getResultCode(); | |||
if (resultCode != null && resultCode == 0) { | |||
String pressKey = callback.getPressKey(); | |||
if (pressKey != null) { | |||
pressKey = pressKey.replaceAll("\\*", "").trim(); | |||
} | |||
if (StrUtils.isBlank(pressKey) && waiting) { | |||
return Optional.empty(); | |||
} | |||
status = AGREE_KEY.equals(pressKey) ? AGREED : REFUSED; | |||
} else { | |||
if (waiting) { | |||
return Optional.empty(); | |||
} | |||
status = REFUSED; | |||
if (mcr != null || isOvertime) { | |||
if (isOvertime) { | |||
status = ExpertAttendStatusEnum.UNANSWERED; | |||
} else { | |||
ReplyStatus replyStatus = mcr.getReplyStatus(); | |||
switch (replyStatus) { | |||
case OVERTIME: | |||
case FAILED: | |||
status = ExpertAttendStatusEnum.UNANSWERED; | |||
break; | |||
case SUCCESS: | |||
String reply = StrUtil.trim(mcr.getReplyContent()); | |||
if (AGREE_KEY.equals(reply)) { | |||
status = ExpertAttendStatusEnum.AGREED; | |||
} else { | |||
status = ExpertAttendStatusEnum.REFUSED; | |||
} | |||
break; | |||
default: | |||
status = null; | |||
break; | |||
} | |||
} catch (Exception e) { | |||
log.error("获取电话回调结果异常{}", mrd, e); | |||
status = UNANSWERED; | |||
} | |||
} else { | |||
// 超时未回复设置为拒绝参加 | |||
status = REFUSED; | |||
status = null; | |||
} | |||
return Optional.of(status.getCode()); | |||
return Optional.ofNullable(status); | |||
} | |||
private static Optional<Integer> getStatusByMsgRecordDetail(SmsReplyDetails smsReplyDetails, int minutes, MeetingExpert expert) { | |||
Map<String, List<SmsReply>> accept = smsReplyDetails.getAccept() //回复1同意参加的手机号 | |||
, reject = smsReplyDetails.getReject() //回复2拒绝参加的手机号 | |||
, other = smsReplyDetails.getOther(); //回复其他的专家 | |||
Map<String, List<String>> errorPhones = new HashMap<>();//发送失败的专家 | |||
// 专家最长响应时间 | |||
LocalDateTime limitTime = LocalDateTime.now().minusMinutes(minutes); | |||
// 专家抽取时间 | |||
boolean waiting = limitTime.isBefore(expert.getCreateOn()); | |||
ExpertAttendStatusEnum status = NOTICING; | |||
boolean hasCallBack = ObjectUtil.isEmpty(smsReplyDetails); | |||
if (hasCallBack && waiting) { | |||
return Optional.empty(); | |||
} | |||
// if (!waiting) { | |||
// status = REFUSED; | |||
// } | |||
List<String> errorPhoneList = errorPhones.get(expert.getSmsUuid()); | |||
if(ObjectUtil.isNotEmpty(errorPhoneList) && errorPhoneList.contains(expert.getMobile())){ | |||
// 未应答 | |||
status = UNANSWERED; | |||
} | |||
if(ObjectUtil.isNotEmpty(accept)) { | |||
List<SmsReply> smsRepliesAccept = accept.get(expert.getSmsUuid()); | |||
if (ObjectUtil.isNotEmpty(smsRepliesAccept)) { | |||
boolean containsReplyMobile = smsRepliesAccept.stream() | |||
.anyMatch(reply -> reply.getReplyMobile().equals(expert.getMobile())); | |||
if (containsReplyMobile) { | |||
SmsReply filteredReplies = smsRepliesAccept.stream() | |||
.filter(reply -> reply.getReplyMobile().equals(expert.getMobile())) | |||
.collect(Collectors.toList()).get(0); | |||
// 回复时间 | |||
Instant instant = Instant.ofEpochMilli(filteredReplies.getReplyReplytime()); | |||
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); | |||
if (localDateTime.isBefore(expert.getCreateOn().plusMinutes(minutes))) { | |||
status = AGREED; | |||
} else { | |||
status = REFUSED; | |||
} | |||
} | |||
} | |||
} | |||
if(ObjectUtil.isNotEmpty(reject)) { | |||
List<SmsReply> smsRepliesReject = reject.get(expert.getSmsUuid()); | |||
if (ObjectUtil.isNotEmpty(smsRepliesReject)) { | |||
boolean containsReplyMobile = smsRepliesReject.stream() | |||
.anyMatch(reply -> reply.getReplyMobile().equals(expert.getMobile())); | |||
if (containsReplyMobile) { | |||
status = REFUSED; | |||
} | |||
} | |||
} | |||
if(ObjectUtil.isNotEmpty(other)) { | |||
List<SmsReply> smsRepliesOther = other.get(expert.getSmsUuid()); | |||
if (ObjectUtil.isNotEmpty(smsRepliesOther)) { | |||
boolean containsReplyMobile = smsRepliesOther.stream() | |||
.anyMatch(reply -> reply.getReplyMobile().equals(expert.getMobile())); | |||
if (containsReplyMobile) { | |||
status = REFUSED; | |||
} | |||
} | |||
} | |||
return Optional.of(status.getCode()); | |||
} | |||
// 发送短信提醒 | |||
public String sendExpertSms(List<MeetingExpert> expertMeetings, Meeting meeting) { | |||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |||
// 短信内容 | |||
String replacedContent = String.format(VoiceSmsTemplateConst.EXPERT_INVITE, | |||
meeting.getCreator(), meeting.getName(), meeting.getStartTime().format(formatter) + "至" + meeting.getEndTime().format(formatter), meeting.getMeetingAddress()); | |||
List<String> phones = expertMeetings.stream().map(obj -> obj.getMobile()).collect(Collectors.toList()); | |||
SmsDTO<SmsSendResponse> smsSendResponseSmsDto = smsServiceClient.smsSend(replacedContent, phones); | |||
// 短信发送成功返回的UUID | |||
String resultUuid = smsSendResponseSmsDto.getData().getResult(); | |||
// 保存短信发送记录 | |||
MeetingExpertSms meetingExpertSms = MeetingExpertSms.builder() | |||
.meetingId(meeting.getId().toString()) | |||
.createOn(LocalDateTime.now()) | |||
.smsUuid(resultUuid) | |||
.content(replacedContent) | |||
.phones(phones.stream() | |||
.collect(Collectors.joining(", "))) | |||
.smsResult(smsSendResponseSmsDto.toString()) | |||
.build(); | |||
meetingExpertSmsMapper.insert(meetingExpertSms); | |||
return resultUuid; | |||
} | |||
// 查看短信回复内容并更改专家回复状态 | |||
public SmsReplyDetails viewSmsReplies(List<String> uuids) { | |||
Map<String, List<SmsReply>> accept = new HashMap<>() | |||
, reject = new HashMap<>() | |||
, other = new HashMap<>(); | |||
Map<String, List<String>> errorPhones = new HashMap<>(); | |||
uuids.forEach(uuid -> { | |||
SmsReplyResponse response = smsServiceClient.smsReply(uuid); | |||
List<SmsReply> smsReplies = response.getResult(); | |||
// 成功回复的手机信息 | |||
if (ObjectUtil.isNotEmpty(smsReplies)){ | |||
// 只有第一条回复的内容有效 | |||
smsReplies = smsReplies.stream() | |||
.collect(Collectors.groupingBy(SmsReply::getReplyMobile, | |||
Collectors.minBy(Comparator.comparing(SmsReply::getReplyReplytime)))) | |||
.values().stream() | |||
.map(Optional::get) | |||
.collect(Collectors.toList()); | |||
accept.put(uuid, smsReplies.stream() | |||
.filter(obj -> ObjectUtil.isNotEmpty(obj)) | |||
.filter(smsReply -> "1".equals(smsReply.getReplyContent())) | |||
.collect(Collectors.toList())); | |||
reject.put(uuid, smsReplies.stream() | |||
.filter(obj -> ObjectUtil.isNotEmpty(obj)) | |||
.filter(smsReply -> "2".equals(smsReply.getReplyContent())) | |||
.collect(Collectors.toList())); | |||
other.put(uuid, smsReplies.stream() | |||
.filter(obj -> ObjectUtil.isNotEmpty(obj)) | |||
.filter(smsReply -> !("1".equals(smsReply.getReplyContent()) || "2".equals(smsReply.getReplyContent()))) | |||
.collect(Collectors.toList())); | |||
} | |||
// 发送失败的手机号 | |||
if (StrUtils.isNotBlank(response.getErrorPhone())){ | |||
errorPhones.put(uuid, Arrays.asList(response.getErrorPhone().split(","))); | |||
} | |||
}); | |||
SmsReplyDetails smsReplyDetails = SmsReplyDetails.builder() | |||
.accept(accept) | |||
.reject(reject) | |||
.errorPhones(errorPhones).build(); | |||
return smsReplyDetails; | |||
} | |||
} |
@@ -14,7 +14,7 @@ import com.hz.pm.api.meeting.entity.dto.InviteCacheDTO; | |||
import com.hz.pm.api.meeting.entity.dto.RandomInviteRuleDTO; | |||
import com.hz.pm.api.meeting.entity.enumeration.ExpertAttendStatusEnum; | |||
import com.hz.pm.api.meeting.entity.req.ConfirmedRosterReq; | |||
import com.hz.pm.api.meeting.helper.MeetingCallOrMsgHelper; | |||
import com.hz.pm.api.meeting.helper.MeetingNotifyHelper; | |||
import com.hz.pm.api.meeting.manage.ExpertInviteManage; | |||
import com.hz.pm.api.meeting.manage.MeetingManage; | |||
import com.hz.pm.api.meeting.service.IExpertInviteAvoidRuleService; | |||
@@ -72,7 +72,7 @@ public class ExpertRandomInviteTask { | |||
private final IMeetingService meetingService; | |||
private final ExpertInviteManage expertInviteManage; | |||
private final IExpertInviteAvoidRuleService inviteAvoidRuleService; | |||
private final MeetingCallOrMsgHelper meetingCallOrMsgHelper; | |||
private final MeetingNotifyHelper meetingNotifyHelper; | |||
/** | |||
* 用来存入线程执行句柄, 停止定时任务时使用 | |||
@@ -227,7 +227,7 @@ public class ExpertRandomInviteTask { | |||
public void invite(Long meetingId, Boolean reInvite, LocalDateTime tsTime) { | |||
log.info("开始进行专家后台抽取:{}", meetingId); | |||
if (!inInviteTimeRange()) { | |||
log.warn("不在会议抽取执行时间:{}",meetingId); | |||
log.warn("不在会议抽取执行时间:{}", meetingId); | |||
return; | |||
} | |||
Meeting meeting = meetingService.getById(meetingId); | |||
@@ -268,9 +268,8 @@ public class ExpertRandomInviteTask { | |||
expert.setStatus(ExpertAttendStatusEnum.NOTICING.getCode()); | |||
return expert; | |||
}); | |||
// meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertMeetings); | |||
// 短信或电话提醒 | |||
meetingCallOrMsgHelper.smsOrCallExpertByMeeting(meeting, expertMeetings); | |||
meetingNotifyHelper.notifyExperts(meeting, expertMeetings); | |||
log.info("会议:{} 后台抽取专家:{}名", meetingId, expertMeetings.size()); | |||
meetingExpertService.saveBatch(expertMeetings); | |||
} else { | |||
@@ -283,7 +282,7 @@ public class ExpertRandomInviteTask { | |||
meetingService.stopRandomInvite(meetingId); | |||
if (notIgnoreCnt.get() == notSupportCnt.get() && notIgnoreCnt.get() > 0) { | |||
// 当未完成抽取且无专家可抽取时 | |||
meetingCallOrMsgHelper.sendInviteStopMsg(meeting.getCreateBy(), meetingId, meeting.getName()); | |||
meetingNotifyHelper.sendInviteStopMsg(meeting.getCreateBy(), meetingId, meeting.getName()); | |||
} | |||
} | |||
// 所有抽取规则抽取人数满足 自动召开会议 | |||
@@ -0,0 +1,171 @@ | |||
package com.hz.pm.api.meeting.task; | |||
import cn.hutool.core.collection.CollUtil; | |||
import cn.hutool.core.date.LocalDateTimeUtil; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.baomidou.mybatisplus.core.conditions.Wrapper; | |||
import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |||
import com.hz.pm.api.common.helper.MsgCallHelper; | |||
import com.hz.pm.api.common.util.BizUtils; | |||
import com.hz.pm.api.common.util.MDCThreadPoolTaskExecutor; | |||
import com.hz.pm.api.external.sms.vo.SmsReply; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyDTO; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord.ReplyStatus; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord.SendStatus; | |||
import com.hz.pm.api.sys.model.enumeration.SubmitTypeEnum; | |||
import com.hz.pm.api.sys.service.IMsgCallRecordService; | |||
import com.ningdatech.basic.util.CollUtils; | |||
import lombok.RequiredArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.scheduling.annotation.Scheduled; | |||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | |||
import org.springframework.stereotype.Component; | |||
import java.time.Instant; | |||
import java.time.LocalDateTime; | |||
import java.util.*; | |||
import java.util.concurrent.ThreadPoolExecutor; | |||
import java.util.concurrent.TimeUnit; | |||
import static com.hz.pm.api.external.MobileCallClient.ReplyCallDTO; | |||
/** | |||
* <p> | |||
* MsgCallReplyRewriteTask | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 21:29 2024/4/25 | |||
*/ | |||
@Slf4j | |||
@Component | |||
public class MsgCallReplyRewriteTask { | |||
private static final ThreadPoolTaskExecutor EXECUTOR; | |||
static { | |||
// 初始化线程池 | |||
EXECUTOR = new MDCThreadPoolTaskExecutor(); | |||
EXECUTOR.setCorePoolSize(2); | |||
EXECUTOR.setMaxPoolSize(4); | |||
EXECUTOR.setQueueCapacity(100); | |||
EXECUTOR.setKeepAliveSeconds(120); | |||
EXECUTOR.setThreadPriority(Thread.NORM_PRIORITY); | |||
EXECUTOR.setWaitForTasksToCompleteOnShutdown(true); | |||
EXECUTOR.setThreadNamePrefix("callRewriteExecutor-"); | |||
EXECUTOR.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); | |||
} | |||
private final IMsgCallRecordService msgCallRecordService; | |||
private final MsgCallHelper msgCallHelper; | |||
public MsgCallReplyRewriteTask(IMsgCallRecordService msgCallRecordService, MsgCallHelper msgCallHelper) { | |||
this.msgCallRecordService = msgCallRecordService; | |||
this.msgCallHelper = msgCallHelper; | |||
EXECUTOR.initialize(); | |||
} | |||
@Scheduled(fixedRateString = "${msg-call-reply-write.fixed-rate:2}", timeUnit = TimeUnit.MINUTES) | |||
public void replyRewrite() { | |||
log.info("开始获取短信电话回执"); | |||
Wrapper<MsgCallRecord> query = Wrappers.lambdaQuery(MsgCallRecord.class) | |||
.eq(MsgCallRecord::getNeedReply, Boolean.TRUE) | |||
.eq(MsgCallRecord::getSendStatus, SendStatus.SUCCESS) | |||
.eq(MsgCallRecord::getReplyStatus, ReplyStatus.WAITING) | |||
.isNull(MsgCallRecord::getReplyOn); | |||
List<MsgCallRecord> records = msgCallRecordService.list(query); | |||
if (records.isEmpty()) { | |||
return; | |||
} | |||
LocalDateTime startTime = LocalDateTime.now().minusMinutes(60); | |||
Map<Boolean, List<MsgCallRecord>> map = CollUtils.group(records, w -> w.getCreateOn().isBefore(startTime)); | |||
List<MsgCallRecord> overtimeRecords = map.get(Boolean.TRUE); | |||
if (CollUtil.isNotEmpty(overtimeRecords)) { | |||
overtimeRecords.forEach(w -> { | |||
w.setReplyStatus(ReplyStatus.OVERTIME); | |||
w.setReplyOn(LocalDateTime.now()); | |||
}); | |||
msgCallRecordService.updateBatchById(overtimeRecords); | |||
} | |||
List<MsgCallRecord> waitingRecords = map.get(Boolean.FALSE); | |||
if (CollUtil.isNotEmpty(waitingRecords)) { | |||
CollUtils.group(waitingRecords, MsgCallRecord::getSubmitType) | |||
.forEach((submitType, recordsBySummitType) -> { | |||
if (SubmitTypeEnum.CALL.equals(submitType)) { | |||
rewriteCallReply(recordsBySummitType); | |||
} else if (SubmitTypeEnum.SMS.equals(submitType)) { | |||
rewriteSmsReply(recordsBySummitType); | |||
} | |||
}); | |||
} | |||
} | |||
private void rewriteSmsReply(List<MsgCallRecord> records) { | |||
CollUtils.group(records, MsgCallRecord::getSubmitKey) | |||
.forEach((submitKey, recordsBySubmitKey) -> { | |||
Runnable function = () -> { | |||
try { | |||
SmsReplyDTO retReply = msgCallHelper.smsReply(submitKey); | |||
String errorPhoneStr = retReply.getErrorPhone(); | |||
List<String> errorPhones = StrUtil.split(errorPhoneStr, ","); | |||
List<SmsReply> replies = retReply.getResult(); | |||
Map<String, SmsReply> currReplyMap = new HashMap<>(); | |||
if (replies != null && !replies.isEmpty()) { | |||
replies.sort(Comparator.comparing(SmsReply::getReplyReplytime)); | |||
replies.forEach(w -> currReplyMap.putIfAbsent(w.getReplyMobile(), w)); | |||
} | |||
List<MsgCallRecord> updates = new ArrayList<>(); | |||
for (MsgCallRecord w : recordsBySubmitKey) { | |||
if (errorPhones.contains(w.getReceivePhone())) { | |||
w.setReplyStatus(ReplyStatus.FAILED); | |||
w.setReplyOn(LocalDateTime.now()); | |||
} else if (currReplyMap.containsKey(w.getReceivePhone())) { | |||
SmsReply reply = currReplyMap.get(w.getReceivePhone()); | |||
w.setReplyOn(LocalDateTime.from(Instant.ofEpochMilli(reply.getReplyReplytime()))); | |||
w.setReplyContent(reply.getReplyContent()); | |||
w.setReplyStatus(ReplyStatus.SUCCESS); | |||
} | |||
if (w.getReplyOn() != null) { | |||
updates.add(w); | |||
} | |||
} | |||
msgCallRecordService.updateBatchById(updates); | |||
} catch (Exception e) { | |||
log.error("回复结果回填失败:{}", submitKey, e); | |||
} | |||
}; | |||
EXECUTOR.execute(function); | |||
}); | |||
} | |||
private void rewriteCallReply(List<MsgCallRecord> records) { | |||
CollUtils.group(records, MsgCallRecord::getSubmitKey) | |||
.forEach((submitKey, recordsBySubmitKey) -> { | |||
Runnable function = () -> { | |||
try { | |||
List<ReplyCallDTO> retReplies = msgCallHelper.callReply(submitKey); | |||
Map<String, ReplyCallDTO> replyMap = BizUtils.groupFirstMap(retReplies, | |||
ReplyCallDTO::getRemoteNumber, | |||
Comparator.comparing(ReplyCallDTO::getSendDate)); | |||
List<MsgCallRecord> updates = new ArrayList<>(); | |||
for (MsgCallRecord w : recordsBySubmitKey) { | |||
ReplyCallDTO reply = replyMap.get(w.getReceivePhone()); | |||
if (reply != null) { | |||
w.setReplyOn(LocalDateTimeUtil.of(reply.getSendDate())); | |||
w.setReplyContent(reply.getDtmf()); | |||
w.setReplyStatus(ReplyStatus.SUCCESS); | |||
updates.add(w); | |||
} | |||
} | |||
msgCallRecordService.updateBatchById(updates); | |||
} catch (Exception e) { | |||
log.error("回复结果回填失败:{}", submitKey, e); | |||
} | |||
}; | |||
EXECUTOR.execute(function); | |||
}); | |||
} | |||
} |
@@ -599,33 +599,35 @@ public class ConstructionManage { | |||
purchase.setXcfhxApplyFiles(req.getXcfhxApplyFiles()); | |||
purchase.setMatchXcfhx(req.getMatchXcfhx()); | |||
purchase.setXcfhxApplyRemark(req.getXcfhxApplyRemark()); | |||
if (StrUtils.isBlank(purchase.getMhXcfhxReportFile()) || | |||
req.getMhXcfhxReportFile().equals(purchase.getMhXcfhxReportFile())) { | |||
ApiResponse<FileResultVO> retFileInfo; | |||
File tmpFile = null; | |||
FileInputStream fis = null; | |||
try { | |||
tmpFile = mhFileClient.downloadToTmpFile(req.getMhXcfhxReportFile()); | |||
String fileName = purchase.getBidName() + "-信创符合性测评报告." + FileUtil.getSuffix(tmpFile); | |||
fis = new FileInputStream(tmpFile); | |||
String mimeType = FileUtil.getMimeType(tmpFile.getPath()); | |||
MockMultipartFile multipartFile = new MockMultipartFile(fileName, fileName, mimeType, fis); | |||
retFileInfo = fileController.upload(multipartFile, "default"); | |||
} catch (IOException e) { | |||
log.error("信创报告上传失败", e); | |||
throw BizException.wrap("信创符合性测评报告上传失败"); | |||
} finally { | |||
if (tmpFile != null) { | |||
tmpFile.deleteOnExit(); | |||
} | |||
if (fis != null) { | |||
IOUtils.closeQuietly(fis); | |||
if (Boolean.TRUE.equals(req.getMatchXcfhx())) { | |||
if (StrUtils.isBlank(purchase.getMhXcfhxReportFile()) || | |||
req.getMhXcfhxReportFile().equals(purchase.getMhXcfhxReportFile())) { | |||
ApiResponse<FileResultVO> retFileInfo; | |||
File tmpFile = null; | |||
FileInputStream fis = null; | |||
try { | |||
tmpFile = mhFileClient.downloadToTmpFile(req.getMhXcfhxReportFile()); | |||
String fileName = purchase.getBidName() + "-信创符合性测评报告." + FileUtil.getSuffix(tmpFile); | |||
fis = new FileInputStream(tmpFile); | |||
String mimeType = FileUtil.getMimeType(tmpFile.getPath()); | |||
MockMultipartFile multipartFile = new MockMultipartFile(fileName, fileName, mimeType, fis); | |||
retFileInfo = fileController.upload(multipartFile, "default"); | |||
} catch (IOException e) { | |||
log.error("信创报告上传失败", e); | |||
throw BizException.wrap("信创符合性测评报告上传失败"); | |||
} finally { | |||
if (tmpFile != null) { | |||
tmpFile.deleteOnExit(); | |||
} | |||
if (fis != null) { | |||
IOUtils.closeQuietly(fis); | |||
} | |||
} | |||
purchase.setXcfhxReportFiles(JSONUtil.toJsonStr(retFileInfo)); | |||
} | |||
purchase.setXcfhxReportFiles(JSONUtil.toJsonStr(retFileInfo)); | |||
purchase.setMhXcfhxReportRecordId(req.getMhXcfhxReportRecordId()); | |||
purchase.setMhXcfhxReportFile(req.getMhXcfhxReportFile()); | |||
} | |||
purchase.setMhXcfhxReportRecordId(req.getMhXcfhxReportRecordId()); | |||
purchase.setMhXcfhxReportFile(req.getMhXcfhxReportFile()); | |||
xcfhxStateMachineUtil.pass(purchase); | |||
purchaseService.updateById(purchase); | |||
@@ -26,11 +26,9 @@ public class XcfhxApplyReq { | |||
private Long bidId; | |||
@ApiModelProperty("信创符合性测评报告文件") | |||
@NotBlank(message = "信创符合性测评报告文件不能为空") | |||
private String mhXcfhxReportFile; | |||
@ApiModelProperty("信创符合性测评记录ID") | |||
@NotBlank(message = "信创符合性测评记录ID不能为空") | |||
private String mhXcfhxReportRecordId; | |||
@ApiModelProperty("是否符合信创符合性要求") | |||
@@ -2,18 +2,18 @@ package com.hz.pm.api.sms.manage; | |||
import cn.hutool.core.util.PhoneUtil; | |||
import cn.hutool.core.util.RandomUtil; | |||
import com.hz.pm.api.common.helper.MsgCallHelper; | |||
import com.hz.pm.api.external.sms.MhSmsClient; | |||
import com.hz.pm.api.sms.constant.VerificationCodeType; | |||
import com.hz.pm.api.sms.constant.VoiceSmsTemplateConst; | |||
import com.hz.pm.api.sms.model.dto.VerifyCodeCacheDTO; | |||
import com.hz.pm.api.sms.model.po.ReqVerificationCodePO; | |||
import com.hz.pm.api.sms.utils.DateUtil; | |||
import com.hz.pm.api.sms.utils.SmsRedisKeyUtils; | |||
import com.hz.pm.api.sys.model.enumeration.BizTypeEnum; | |||
import com.ningdatech.basic.exception.BizException; | |||
import com.ningdatech.cache.model.cache.CacheKey; | |||
import com.ningdatech.cache.repository.CachePlusOps; | |||
import com.ningdatech.yxt.client.YxtClient; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import lombok.RequiredArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.commons.lang3.StringUtils; | |||
@@ -34,7 +34,7 @@ import java.util.Objects; | |||
@RequiredArgsConstructor | |||
public class VerificationCodeManage { | |||
private final YxtClient yxtClient; | |||
private final MsgCallHelper msgCallHelper; | |||
private final CachePlusOps cachePlusOps; | |||
public void sendVerificationCode(ReqVerificationCodePO req) { | |||
@@ -62,22 +62,15 @@ public class VerificationCodeManage { | |||
.build(); | |||
// 创建短信内容 | |||
SendSmsCmd sendSmsCmd = new SendSmsCmd(); | |||
switch (codeType) { | |||
case LOGIN: { | |||
SendSmsContext sendSmsCtx = new SendSmsContext(); | |||
sendSmsCtx.setReceiveNumber(req.getMobile()); | |||
sendSmsCtx.setContent(String.format(VoiceSmsTemplateConst.SMS_VERIFY_CODE, code, codeType.getExpireTime())); | |||
sendSmsCmd.setContextList(Collections.singletonList(sendSmsCtx)); | |||
// sendSmsCmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
String content = String.format(VoiceSmsTemplateConst.SMS_VERIFY_CODE, code, codeType.getExpireTime()); | |||
msgCallHelper.sendMsg(content, req.getMobile(), BizTypeEnum.VERIFY_CODE); | |||
} | |||
break; | |||
case EXPERT_REGISTER: { | |||
SendSmsContext sendSmsCtx = new SendSmsContext(); | |||
sendSmsCtx.setReceiveNumber(req.getMobile()); | |||
sendSmsCtx.setContent(String.format(VoiceSmsTemplateConst.EXPERT_REGISTER, code, codeType.getExpireTime())); | |||
sendSmsCmd.setContextList(Collections.singletonList(sendSmsCtx)); | |||
// sendSmsCmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
String content = String.format(VoiceSmsTemplateConst.EXPERT_REGISTER, code, codeType.getExpireTime()); | |||
msgCallHelper.sendMsg(content, req.getMobile(), BizTypeEnum.VERIFY_CODE); | |||
} | |||
break; | |||
default: | |||
@@ -85,7 +78,6 @@ public class VerificationCodeManage { | |||
} | |||
// 发送 短信 | |||
yxtClient.submitSmsTask(sendSmsCmd); | |||
log.info("发送短信验证码:{} -> {}", req.getMobile(), code); | |||
cachePlusOps.set(new CacheKey(cacheKey, Duration.ofMinutes(codeType.getExpireTime())), cache); | |||
@@ -1,28 +0,0 @@ | |||
package com.hz.pm.api.sms.task; | |||
import com.ningdatech.yxt.client.YxtContext; | |||
import lombok.RequiredArgsConstructor; | |||
import org.springframework.scheduling.annotation.Scheduled; | |||
import org.springframework.stereotype.Component; | |||
/** | |||
* @author liuxinxin | |||
* @date 2022/8/9 下午3:58 | |||
* 音信通定时检查电话结果 | |||
*/ | |||
@Component | |||
@RequiredArgsConstructor | |||
public class YxtPollingTask { | |||
private final YxtContext yxtContext; | |||
/** | |||
* 校验音信通结果数据 | |||
* 每5分钟执行一次 | |||
*/ | |||
@Scheduled(cron = "0 */1 * * * ?") | |||
public void smsMsgResultCheck() { | |||
yxtContext.smsMsgResultCheck(); | |||
} | |||
} |
@@ -0,0 +1,18 @@ | |||
package com.hz.pm.api.sys.controller; | |||
import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.stereotype.Controller; | |||
/** | |||
* <p> | |||
* 前端控制器 | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 2024-04-25 | |||
*/ | |||
@Controller | |||
@RequestMapping("/api.sys/ndMsgCallRecord") | |||
public class NdMsgCallRecordController { | |||
} |
@@ -1,8 +1,6 @@ | |||
package com.hz.pm.api.sys.manage; | |||
import com.hz.pm.api.common.enumeration.CommonEnum; | |||
import com.hz.pm.api.common.helper.UserInfoHelper; | |||
import com.hz.pm.api.meeting.helper.YxtClientHelper; | |||
import com.hz.pm.api.projectlib.model.entity.Project; | |||
import com.hz.pm.api.projectlib.model.enumeration.InstTypeEnum; | |||
import com.hz.pm.api.projectlib.model.enumeration.WarningFlowTypeEnum; | |||
@@ -13,7 +11,6 @@ import com.hz.pm.api.sys.model.entity.WflowEarlyWarningRecords; | |||
import com.hz.pm.api.sys.service.IEarlyWarningRecordsService; | |||
import com.hz.pm.api.sys.service.INotifyService; | |||
import com.hz.pm.api.user.security.model.UserFullInfoDTO; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import com.wflow.enums.WarningNoticeTypeEnum; | |||
import com.wflow.enums.WarningRuleTypeEnum; | |||
import lombok.AllArgsConstructor; | |||
@@ -37,13 +34,8 @@ import java.util.Objects; | |||
public class EarlyWarningManage { | |||
private final UserInfoHelper userInfoHelper; | |||
private final NoticeManage noticeManage; | |||
private final IEarlyWarningRecordsService earlyWarningRecordsService; | |||
private final YxtClientHelper yxtClientHelper; | |||
private final INotifyService notifyService; | |||
/** | |||
@@ -147,14 +139,6 @@ public class EarlyWarningManage { | |||
Notify notify = noticeManage.assemblyAuditNotify(user.getUserId(), project, content); | |||
notify.setType(MsgTypeEnum.PROJECT_REVIEW.name()); | |||
notifyService.save(notify); | |||
//3.发短信 | |||
if (noticeMethod.contains(String.valueOf(CommonEnum.MOBILE.getCode()))) { | |||
SendSmsContext context = new SendSmsContext(); | |||
context.setReceiveNumber(user.getMobile()); | |||
context.setContent(content); | |||
yxtClientHelper.sendSms(context); | |||
} | |||
} | |||
/** | |||
@@ -263,13 +247,6 @@ public class EarlyWarningManage { | |||
notify.setType(MsgTypeEnum.PROJECT_REVIEW.name()); | |||
notifyService.save(notify); | |||
//3.发短信 | |||
if (noticeMethod.contains(String.valueOf(CommonEnum.MOBILE.getCode()))) { | |||
SendSmsContext context = new SendSmsContext(); | |||
context.setReceiveNumber(user.getMobile()); | |||
context.setContent(content); | |||
yxtClientHelper.sendSms(context); | |||
} | |||
} | |||
/** | |||
@@ -0,0 +1,16 @@ | |||
package com.hz.pm.api.sys.mapper; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
/** | |||
* <p> | |||
* Mapper 接口 | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 2024-04-25 | |||
*/ | |||
public interface MsgCallRecordMapper extends BaseMapper<MsgCallRecord> { | |||
} |
@@ -0,0 +1,5 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
<mapper namespace="com.hz.pm.api.sys.mapper.MsgCallRecordMapper"> | |||
</mapper> |
@@ -0,0 +1,124 @@ | |||
package com.hz.pm.api.sys.model.entity; | |||
import com.baomidou.mybatisplus.annotation.*; | |||
import com.hz.pm.api.sys.model.enumeration.BizTypeEnum; | |||
import com.hz.pm.api.sys.model.enumeration.SubmitTypeEnum; | |||
import io.swagger.annotations.ApiModel; | |||
import lombok.Data; | |||
import java.io.Serializable; | |||
import java.time.LocalDateTime; | |||
/** | |||
* <p> | |||
* 短信语音记录表 | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 2024-04-25 | |||
*/ | |||
@Data | |||
@TableName("ND_MSG_CALL_RECORD") | |||
@ApiModel(value = "NdMsgCallRecord对象") | |||
public class MsgCallRecord implements Serializable { | |||
private static final long serialVersionUID = 1L; | |||
@TableId(value = "ID", type = IdType.AUTO) | |||
private Long id; | |||
@TableField(fill = FieldFill.INSERT) | |||
private LocalDateTime createOn; | |||
@TableField(fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updateOn; | |||
@TableField(fill = FieldFill.INSERT) | |||
private Long createBy; | |||
@TableField(fill = FieldFill.INSERT_UPDATE) | |||
private Long updateBy; | |||
/** | |||
* 业务类型 | |||
*/ | |||
private BizTypeEnum bizType; | |||
/** | |||
* 提交标识 | |||
*/ | |||
private String submitKey; | |||
/** | |||
* 提交类型 | |||
*/ | |||
private SubmitTypeEnum submitType; | |||
/** | |||
* 接收手机号 | |||
*/ | |||
private String receivePhone; | |||
/** | |||
* 短信内容 | |||
*/ | |||
private String content; | |||
/** | |||
* 模板编码 | |||
*/ | |||
private String templateCode; | |||
/** | |||
* 是否需要回复 | |||
*/ | |||
private Boolean needReply; | |||
/** | |||
* 发送状态: | |||
*/ | |||
private SendStatus sendStatus; | |||
/** | |||
* 回复状态 | |||
*/ | |||
private ReplyStatus replyStatus; | |||
/** | |||
* 回复内容 | |||
*/ | |||
private String replyContent; | |||
/** | |||
* 回复时间 | |||
*/ | |||
private LocalDateTime replyOn; | |||
/** | |||
* 发送状态 | |||
*/ | |||
public enum SendStatus { | |||
SUCCESS, | |||
FAILED | |||
} | |||
/** | |||
* 发送状态 | |||
*/ | |||
public enum ReplyStatus { | |||
SUCCESS, | |||
WAITING, | |||
OVERTIME, | |||
FAILED | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
package com.hz.pm.api.sys.model.enumeration; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Getter; | |||
/** | |||
* <p> | |||
* BizTypeEnum | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 16:13 2024/4/25 | |||
*/ | |||
@Getter | |||
@AllArgsConstructor | |||
public enum BizTypeEnum { | |||
VERIFY_CODE("验证码"), | |||
MEETING_ROSTER_CONFIRM("会议名单确认"), | |||
EXPERT_LEAVE("专家请假"), | |||
MEETING_CANCEL("会议取消"), | |||
EXPERT_INVITE_STOP("会议取消"), | |||
EXPERT_INVITE("专家邀请"); | |||
private final String desc; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.hz.pm.api.sys.model.enumeration; | |||
/** | |||
* <p> | |||
* MsgTypeEnum | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 15:26 2024/4/25 | |||
*/ | |||
public enum SubmitTypeEnum { | |||
SMS, | |||
CALL | |||
} |
@@ -0,0 +1,16 @@ | |||
package com.hz.pm.api.sys.service; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.baomidou.mybatisplus.extension.service.IService; | |||
/** | |||
* <p> | |||
* 服务类 | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 2024-04-25 | |||
*/ | |||
public interface IMsgCallRecordService extends IService<MsgCallRecord> { | |||
} |
@@ -0,0 +1,20 @@ | |||
package com.hz.pm.api.sys.service.impl; | |||
import com.hz.pm.api.sys.model.entity.MsgCallRecord; | |||
import com.hz.pm.api.sys.mapper.MsgCallRecordMapper; | |||
import com.hz.pm.api.sys.service.IMsgCallRecordService; | |||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | |||
import org.springframework.stereotype.Service; | |||
/** | |||
* <p> | |||
* 服务实现类 | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 2024-04-25 | |||
*/ | |||
@Service | |||
public class MsgCallRecordServiceImpl extends ServiceImpl<MsgCallRecordMapper, MsgCallRecord> implements IMsgCallRecordService { | |||
} |
@@ -319,6 +319,12 @@ public class WorkbenchManage { | |||
.like(StrUtil.isNotBlank(req.getBuildOrg()), Project::getBuildOrgName, req.getBuildOrg()) | |||
.like(StrUtil.isNotBlank(req.getProjectName()), Project::getProjectName, req.getProjectName()) | |||
.in(Project::getBuildOrgCode, CollUtils.convert(viewUnitIds, String::valueOf)) | |||
// 评审金额 | |||
.ge(req.getReviewAmountMin() != null, Project::getReviewAmount, req.getReviewAmountMin()) | |||
.le(req.getReviewAmountMax() != null, Project::getReviewAmount, req.getReviewAmountMax()) | |||
// 下达金额 | |||
.ge(req.getApproveAmountMin() != null, Project::getApprovalAmount, req.getApproveAmountMin()) | |||
.ge(req.getApproveAmountMax() != null, Project::getApprovalAmount, req.getApproveAmountMax()) | |||
.eq(Project::getNewest, Boolean.TRUE) | |||
.orderByDesc(Project::getUpdateOn); | |||
switch (req.getProcessNode()) { | |||
@@ -367,6 +373,8 @@ public class WorkbenchManage { | |||
ProjectLibListItemVO item = new ProjectLibListItemVO(); | |||
item.setId(w.getId()); | |||
item.setProjectName(w.getProjectName()); | |||
// 项目申报 | |||
item.setFromType("1"); | |||
item.setProjectCode(w.getProjectCode()); | |||
item.setArea(w.getArea()); | |||
item.setAreaCode(w.getAreaCode()); | |||
@@ -382,6 +390,7 @@ public class WorkbenchManage { | |||
item.setInstCode(w.getInstCode()); | |||
item.setIsHigherSuperOrg(w.getIsHigherSuperOrg()); | |||
item.setApprovedAmount(w.getApprovalAmount()); | |||
item.setReviewAmount(w.getReviewAmount()); | |||
item.setPrePlanProjectId(w.getPrePlanProjectId()); | |||
return item; | |||
}); | |||
@@ -5,6 +5,8 @@ import io.swagger.annotations.ApiModelProperty; | |||
import lombok.Data; | |||
import lombok.EqualsAndHashCode; | |||
import java.math.BigDecimal; | |||
import static com.hz.pm.api.projectlib.handle.AbstractProcessHandle.ProcessNode; | |||
/** | |||
@@ -34,4 +36,16 @@ public class WorkbenchProjectLibReq extends PagePo { | |||
@ApiModelProperty("申报单位名称") | |||
private String buildOrg; | |||
@ApiModelProperty("评审金额最小值") | |||
private BigDecimal reviewAmountMin; | |||
@ApiModelProperty("评审金额最大值") | |||
private BigDecimal reviewAmountMax; | |||
@ApiModelProperty("批复金额") | |||
private BigDecimal approveAmountMin; | |||
@ApiModelProperty("批复金额") | |||
private BigDecimal approveAmountMax; | |||
} |
@@ -154,7 +154,7 @@ sa-token: | |||
is-log: false | |||
yxt: | |||
# wsdl-url: http://115.239.137.23:9501/ws/v1?wsdl | |||
# wsdl-url: http://115.239.137.23:9501/ws/v1?wsdl | |||
wsdl-url: classpath:/wsdl.xml | |||
#账号 | |||
user-code: hzndkj | |||
@@ -194,9 +194,8 @@ auth-code: | |||
agent-login: | |||
proxy: | |||
secret-key: nqkwiqojg7g4eiypr3rb8s7nb4noa8b2 | |||
sms: | |||
client-url: http://10.54.38.13:8081/mh-gateway/auth-single | |||
sms-send: | |||
host: http://10.54.38.13:8081/mh-gateway/auth-single | |||
# 提醒任务 | |||
reminder-task: | |||
@@ -194,6 +194,10 @@ mh: | |||
upload-url: http://10.54.38.13:8081/mh-gateway/oss/oss/uploadFileSkipLogin | |||
purchase-notice: | |||
open: false | |||
sms-send: | |||
host: http://10.54.38.13:8081/mh-gateway/auth-single | |||
sync-mh-company: | |||
open: false | |||
sync-mh-user: | |||
@@ -0,0 +1,42 @@ | |||
package com.hz.pm.api.external; | |||
import com.hz.pm.api.AppTests; | |||
import com.hz.pm.api.common.helper.MsgCallHelper; | |||
import com.hz.pm.api.common.util.EnvironmentUtil; | |||
import com.hz.pm.api.external.model.dto.CallRetDTO; | |||
import com.hz.pm.api.sys.model.enumeration.BizTypeEnum; | |||
import org.junit.jupiter.api.Test; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.core.env.Environment; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import static org.junit.jupiter.api.Assertions.*; | |||
/** | |||
* <p> | |||
* MobileCallClientTest | |||
* </p> | |||
* | |||
* @author WendyYang | |||
* @since 16:34 2024/4/26 | |||
*/ | |||
class MobileCallClientTest extends AppTests { | |||
@Autowired | |||
private MsgCallHelper msgCallHelper; | |||
@Test | |||
void sendCall() { | |||
String format = String.format("【杭州数字信创】尊敬的专家您好,%s现邀请您作为专家参加%s会议,会议时间:%s,会议地点:%s。 确认参加请按 1,拒绝参加请按 2。请您选择", | |||
"信创办", "测试语音", "2024年1月1日 12:30 至 2024年1月1日 12:30", "杭州市"); | |||
String ret = msgCallHelper.sendCall("19530651430", format, BizTypeEnum.EXPERT_INVITE); | |||
System.out.println(ret); | |||
} | |||
@Test | |||
void replyCall() { | |||
System.out.println(msgCallHelper.callReply("3086259928697016320")); | |||
} | |||
} |
@@ -193,5 +193,7 @@ auth-code: | |||
agent-login: | |||
proxy: | |||
secret-key: nqkwiqojg7g4eiypr3rb8s7nb4noa8b2 | |||
sms: | |||
client-url: http://10.54.38.13:8081/mh-gateway/auth-single | |||
sms-send: | |||
host: http://10.54.38.13:8081/mh-gateway/auth-single | |||
mobile-call: | |||
host: http://36.20.16.36:18181/blue_esl_api |
@@ -55,7 +55,7 @@ public class CodeGen { | |||
} | |||
public static void main(String[] args) { | |||
generate("WendyYang", "projectdeclared", PATH_YYD, "nd_project_review"); | |||
generate("WendyYang", "sys", PATH_YYD, "nd_msg_call_record"); | |||
} | |||
} |