@@ -4,11 +4,10 @@ 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.JSONArray; | |||
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.SmsReply; | |||
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; | |||
@@ -38,17 +37,15 @@ public class SmsServiceClient { | |||
* @param result | |||
* @return | |||
*/ | |||
public List<SmsReply> smsReply(String result) { | |||
List<SmsReply> smsReplyList; | |||
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"); | |||
JSONArray result1 = JSON.parseObject(fileData).getJSONArray("result"); | |||
smsReplyList = JSONObject.parseArray(result1.toJSONString(),SmsReply.class); | |||
return smsReplyList; | |||
return JSONUtil.toBean(fileData, new TypeReference<SmsReplyResponse>() { | |||
}, false); | |||
} | |||
/** | |||
@@ -78,6 +75,11 @@ public class SmsServiceClient { | |||
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,8 +1,10 @@ | |||
package com.hz.pm.api.external.sms.vo; | |||
import lombok.Builder; | |||
import lombok.Data; | |||
@Data | |||
@Builder | |||
public class SmsReceipt { | |||
// {"result":{"successPhone":["13721760288"]},"success":true,"message":"","rows":0,"sendTime":1703835660175} | |||
@@ -15,4 +17,6 @@ public class SmsReceipt { | |||
private Integer rows; | |||
private Long sendTime; | |||
private String error; | |||
} |
@@ -4,6 +4,8 @@ import io.swagger.annotations.ApiModel; | |||
import io.swagger.annotations.ApiModelProperty; | |||
import lombok.Data; | |||
import java.util.List; | |||
@Data | |||
@ApiModel("短信回复响应内容") | |||
public class SmsReplyResponse { | |||
@@ -15,7 +17,7 @@ public class SmsReplyResponse { | |||
private String errorPhone; | |||
@ApiModelProperty("回复内容") | |||
private SmsReply result; | |||
private List<SmsReply> result; | |||
@ApiModelProperty("发送时间") | |||
private Integer sendTime; | |||
@@ -13,20 +13,20 @@ public interface MeetingMsgTemplateConst { | |||
/** | |||
* 已结束:自动抽取结束,结束时给会议发起人发送浙政钉工作通知、短信:“注意,xxx会议自动抽取已结束,请及时确认是否召开会议”。 | |||
*/ | |||
String INVITE_END = "注意,%s会议自动抽取已结束,请及时确认是否召开会议"; | |||
String INVITE_END = "【杭州数字信创】注意,%s会议自动抽取已结束,请及时确认是否召开会议"; | |||
/** | |||
* 确认名单:尊敬的【姓名】专家您好,您于【确认时间】接受了信息化项目评审会议邀请,会议时间:【会议时间】,会议地点:【会议地点】。请准时参加评审会议。如有疑问请联系【联系人】(【联系方式】)。 | |||
*/ | |||
String CONFIRMED_ROSTER = "尊敬的%s专家您好,您于%s接受了信息化项目评审会议邀请,会议时间:%s,会议地点:%s。请准时参加评审会议。如有疑问请联系%s(%s)。"; | |||
String CONFIRMED_ROSTER = "【杭州数字信创】尊敬的%s专家您好,您于%s接受了信息化项目评审会议邀请,会议时间:%s,会议地点:%s。请准时参加评审会议。如有疑问请联系%s(%s)。"; | |||
/** | |||
* 会议取消:尊敬的{name}专家,因会议取消说明,原定于开会时间的会议类型会议已取消。给您带来不便,敬请谅解。如有疑问请咨询会议联系人「会议联系人联系方式」。 | |||
*/ | |||
String MEETING_CANCEL = "尊敬的%s专家,原定于%s的%s会议已取消。给您带来不便,敬请谅解。如有疑问请咨询会议联系人「%s」。"; | |||
String MEETING_CANCEL = "【杭州数字信创】尊敬的%s专家,原定于%s的%s会议已取消。给您带来不便,敬请谅解。如有疑问请咨询会议联系人「%s」。"; | |||
String EXPERT_LEAVE_RANDOM = "请注意,%s会议有专家请假,将重新抽取以替换该专家。"; | |||
String EXPERT_LEAVE_RANDOM = "【杭州数字信创】请注意,%s会议有专家请假,将重新抽取以替换该专家。"; | |||
String EXPERT_LEAVE_APPOINT = "请注意,%s会议的%s专家请假,请及时登录系统替换该专家。"; | |||
String EXPERT_LEAVE_APPOINT = "【杭州数字信创】请注意,%s会议的%s专家请假,请及时登录系统替换该专家。"; | |||
} |
@@ -1,11 +1,17 @@ | |||
package com.hz.pm.api.meeting.controller; | |||
import com.ningdatech.basic.model.PagePo; | |||
import com.ningdatech.basic.model.PageVo; | |||
import com.ningdatech.log.annotation.WebLog; | |||
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.SmsReceipt; | |||
import com.hz.pm.api.external.sms.vo.SmsReplyResponse; | |||
import com.hz.pm.api.external.sms.vo.SmsSendResponse; | |||
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; | |||
import com.hz.pm.api.sms.constant.VoiceSmsTemplateConst; | |||
import com.ningdatech.basic.model.PagePo; | |||
import com.ningdatech.basic.model.PageVo; | |||
import com.ningdatech.log.annotation.WebLog; | |||
import io.swagger.annotations.Api; | |||
import io.swagger.annotations.ApiOperation; | |||
import lombok.AllArgsConstructor; | |||
@@ -14,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import javax.validation.Valid; | |||
import java.util.Collections; | |||
import java.util.List; | |||
/** | |||
@@ -32,6 +39,26 @@ 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)); | |||
} | |||
@ApiOperation("短信回复") | |||
@GetMapping("/aplySms") | |||
public SmsReplyResponse smsReply(String uuid) { | |||
return smsServiceClient.smsReply(uuid); | |||
} | |||
@ApiOperation("短信回执") | |||
@GetMapping("/status") | |||
public SmsReceipt smsReceipt(String uuid) { | |||
return smsServiceClient.smsReceipt(uuid); | |||
} | |||
@ApiOperation("会议日历") | |||
@GetMapping("/meetingCalender") | |||
@WebLog(value = "会议日历") | |||
@@ -102,4 +102,7 @@ public class Meeting implements Serializable { | |||
@TableField(fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updateOn; | |||
@ApiModelProperty("通知方式:1电话 2短信") | |||
private String notifyWay; | |||
} |
@@ -43,8 +43,10 @@ public class MeetingExpert implements Serializable { | |||
@ApiModelProperty("邀请规则ID") | |||
private Long ruleId; | |||
@ApiModelProperty("手机号") | |||
private String mobile; | |||
@ApiModelProperty("专家名称") | |||
private String expertName; | |||
@ApiModelProperty("当前状态") | |||
@@ -61,6 +63,12 @@ public class MeetingExpert implements Serializable { | |||
private String submitKey; | |||
@ApiModelProperty("短信发送返回的uuid") | |||
private String smsUuid; | |||
@ApiModelProperty("通知方式:1电话 2短信") | |||
private String notifyWay; | |||
@TableField(fill = FieldFill.INSERT) | |||
private Long createBy; | |||
@@ -0,0 +1,54 @@ | |||
package com.hz.pm.api.meeting.entity.domain; | |||
import com.baomidou.mybatisplus.annotation.IdType; | |||
import com.baomidou.mybatisplus.annotation.TableId; | |||
import com.baomidou.mybatisplus.annotation.TableName; | |||
import io.swagger.annotations.ApiModel; | |||
import lombok.Builder; | |||
import lombok.Data; | |||
import java.time.LocalDateTime; | |||
/** | |||
* @author wangrenkang | |||
* @date 2024-02-20 18:14:29 | |||
*/ | |||
@Data | |||
@Builder | |||
@TableName("MEETING_EXPERT_SMS") | |||
@ApiModel(value = "专家短信提醒记录") | |||
public class MeetingExpertSms { | |||
@TableId(type = IdType.AUTO) | |||
private Long id; | |||
/** | |||
* 会议ID | |||
*/ | |||
private String meetingId; | |||
/** | |||
* 创建时间 | |||
*/ | |||
private LocalDateTime createOn; | |||
/** | |||
* 短信返回的 uuid | |||
*/ | |||
private String smsUuid; | |||
/** | |||
* 短信发送内容 | |||
*/ | |||
private String content; | |||
/** | |||
* 短信发送手机号 | |||
*/ | |||
private String phones; | |||
/** | |||
* 短信返回内容 | |||
*/ | |||
private String smsResult; | |||
} |
@@ -85,4 +85,7 @@ public class MeetingBasicDTO { | |||
@ApiModelProperty("相关材料") | |||
private String attachFiles; | |||
@ApiModelProperty(value = "通知方式:1电话 2短信", example = "2") | |||
private String notifyWay; | |||
} |
@@ -0,0 +1,32 @@ | |||
package com.hz.pm.api.meeting.entity.dto; | |||
import com.hz.pm.api.external.sms.vo.SmsReply; | |||
import io.swagger.annotations.ApiModel; | |||
import io.swagger.annotations.ApiModelProperty; | |||
import lombok.Builder; | |||
import lombok.Data; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* @author wangrenkang | |||
* @date 2024-02-21 14:24:32 | |||
*/ | |||
@Data | |||
@Builder | |||
@ApiModel("专家短信回复信息详情") | |||
public class SmsReplyDetails { | |||
@ApiModelProperty("回复1同意参加的手机号") | |||
private Map<String, List<SmsReply>> accept; | |||
@ApiModelProperty("回复2拒绝参加的手机号") | |||
private Map<String, List<SmsReply>> reject; | |||
@ApiModelProperty("回复其他内容的手机号") | |||
private Map<String, List<SmsReply>> other; | |||
@ApiModelProperty("发送失败的手机号") | |||
private Map<String, List<String>> errorPhones; | |||
} |
@@ -0,0 +1,39 @@ | |||
package com.hz.pm.api.meeting.entity.enumeration; | |||
import lombok.Getter; | |||
import java.util.Arrays; | |||
/** | |||
* @author wangrenkang | |||
* @date 2024-02-20 18:16:46 | |||
*/ | |||
@Getter | |||
public enum ExpertNotifyTypeEnum { | |||
/** | |||
* 专家通知方式 | |||
*/ | |||
CALL("1", "电话通知"), | |||
SMS("2", "短信通知"); | |||
private final String code; | |||
private final String name; | |||
ExpertNotifyTypeEnum(String code, String name) { | |||
this.code = code; | |||
this.name = name; | |||
} | |||
public boolean eq(String code) { | |||
return this.getCode().equals(code); | |||
} | |||
public static ExpertNotifyTypeEnum getByCode(String code) { | |||
return Arrays.stream(values()) | |||
.filter(w -> w.getCode().equals(code)) | |||
.findFirst() | |||
.orElseThrow(() -> new IllegalArgumentException("无效的通知方式")); | |||
} | |||
} |
@@ -107,4 +107,7 @@ public class MeetingDetailBasicVO { | |||
@ApiModelProperty("会议结果附件") | |||
private String resultAttachFiles; | |||
@ApiModelProperty("通知方式:1电话 2短信") | |||
private String notifyWay; | |||
} |
@@ -3,17 +3,17 @@ 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.ningdatech.basic.util.CollUtils; | |||
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; | |||
@@ -22,8 +22,8 @@ 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 com.ningdatech.yxt.model.cmd.SubmitTaskCallCmd.SubmitTaskCallContext; | |||
import lombok.AllArgsConstructor; | |||
import org.springframework.stereotype.Component; | |||
import org.springframework.transaction.annotation.Transactional; | |||
@@ -53,6 +53,7 @@ public class MeetingCallOrMsgHelper { | |||
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); | |||
@@ -174,20 +175,31 @@ 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 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)); | |||
} | |||
} | |||
public void sendExpertLeaveMsg(MeetingExpert expert, Meeting meeting) { | |||
Long userId = meeting.getCreateBy(); | |||
String msgContent; | |||
@@ -0,0 +1,69 @@ | |||
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,7 +1,6 @@ | |||
package com.hz.pm.api.meeting.helper; | |||
import com.ningdatech.yxt.client.YxtClient; | |||
import com.ningdatech.yxt.constants.YxtSmsSignEnum; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import com.ningdatech.yxt.model.cmd.SubmitTaskCallResponse; | |||
@@ -36,7 +35,7 @@ public class YxtClientHelper { | |||
public void sendSms(List<SendSmsContext> smsList) { | |||
SendSmsCmd cmd = new SendSmsCmd(); | |||
cmd.setContextList(smsList); | |||
cmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
// cmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
yxtClient.submitSmsTask(cmd); | |||
} | |||
@@ -576,7 +576,9 @@ public class ExpertInviteManage { | |||
expertInserts.add(expert); | |||
}); | |||
} | |||
meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertInserts); | |||
// meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertInserts); | |||
// 短信或电话提醒 | |||
meetingCallOrMsgHelper.smsOrCallExpertByMeeting(meeting, expertInserts); | |||
} | |||
meetingExpertService.saveBatch(expertInserts); | |||
} | |||
@@ -14,7 +14,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.hz.pm.api.common.helper.RegionCacheHelper; | |||
import com.hz.pm.api.expert.entity.ExpertUserFullInfo; | |||
import com.hz.pm.api.expert.service.IExpertReviewService; | |||
import com.hz.pm.api.expert.service.IExpertUserFullInfoService; | |||
import com.hz.pm.api.gov.service.IBelongOrgService; | |||
import com.hz.pm.api.meeting.builder.ExpertInviteBuilder; | |||
@@ -36,8 +35,8 @@ import com.hz.pm.api.meeting.task.ExpertRandomInviteTask; | |||
import com.hz.pm.api.meta.helper.DictionaryCache; | |||
import com.hz.pm.api.meta.helper.TagCache; | |||
import com.hz.pm.api.organization.service.IDingOrganizationService; | |||
import com.hz.pm.api.projectlib.model.enumeration.ProjectStatusEnum; | |||
import com.hz.pm.api.projectlib.model.entity.Project; | |||
import com.hz.pm.api.projectlib.model.enumeration.ProjectStatusEnum; | |||
import com.hz.pm.api.projectlib.service.IProjectService; | |||
import com.hz.pm.api.sys.model.dto.RegionDTO; | |||
import com.hz.pm.api.user.security.model.UserInfoDetails; | |||
@@ -228,7 +227,9 @@ public class MeetingManage { | |||
Assert.notNull(avoidInfo, "回避信息不能为空"); | |||
// 随机抽取的话则需进行抽取数量校验 | |||
LocalDateTime now = LocalDateTime.now(); | |||
// 专家抽取(会议创建时抽取) | |||
expertInviteManage.expertInviteByMeetingCreate(meeting, randomRules, avoidInfo); | |||
// 创建会议时添加抽取任务 | |||
expertRandomInviteTask.addInviteTaskByMeetingCreate(meeting.getId(), now); | |||
LambdaUpdateWrapper<Meeting> mUpdate = Wrappers.lambdaUpdate(Meeting.class) | |||
.set(Meeting::getInviteStatus, false) | |||
@@ -460,6 +461,7 @@ public class MeetingManage { | |||
.resultDescription(meeting.getResultDescription()) | |||
.resultAttachFiles(meeting.getResultAttachFiles()) | |||
.remark(meeting.getRemark()) | |||
.notifyWay(meeting.getNotifyWay()) | |||
.build(); | |||
if (Boolean.TRUE.equals(meeting.getIsInnerProject())) { | |||
List<MeetingInnerProject> innerProjects = meetingInnerProjectService.listByMeetingId(meetingId); | |||
@@ -0,0 +1,16 @@ | |||
package com.hz.pm.api.meeting.mapper; | |||
/** | |||
* @author wangrenkang | |||
* @date 2024-02-20 18:16:46 | |||
*/ | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpertSms; | |||
import org.springframework.stereotype.Repository; | |||
@Repository | |||
public interface MeetingExpertSmsMapper extends BaseMapper<MeetingExpertSms> { | |||
int insert(MeetingExpertSms meetingExpertSms); | |||
} |
@@ -0,0 +1,10 @@ | |||
<?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.meeting.mapper.MeetingExpertSmsMapper"> | |||
<insert id="insert" parameterType="com.hz.pm.api.meeting.entity.domain.MeetingExpertSms"> | |||
INSERT INTO HZ_PROJECT_MANAGEMENT1.MEETING_EXPERT_SMS | |||
(MEETING_ID, CREATE_ON, SMS_UUID, CONTENT, PHONES, SMS_RESULT) | |||
VALUES (#{meetingId}, #{createOn}, #{smsUuid}, #{content}, #{phones}, #{smsResult}); | |||
</insert> | |||
</mapper> |
@@ -0,0 +1,7 @@ | |||
package com.hz.pm.api.meeting.service; | |||
import com.baomidou.mybatisplus.extension.service.IService; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpertSms; | |||
public interface IMeetingExpertSmsService extends IService<MeetingExpertSms> { | |||
} |
@@ -0,0 +1,13 @@ | |||
package com.hz.pm.api.meeting.service.impl; | |||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | |||
import com.hz.pm.api.meeting.entity.domain.MeetingExpertSms; | |||
import com.hz.pm.api.meeting.mapper.MeetingExpertSmsMapper; | |||
import com.hz.pm.api.meeting.service.IMeetingExpertSmsService; | |||
import org.springframework.stereotype.Service; | |||
@Service | |||
public class MeetingExpertSmsServiceImpl extends ServiceImpl<MeetingExpertSmsMapper, MeetingExpertSms> implements IMeetingExpertSmsService { | |||
} |
@@ -4,16 +4,26 @@ import cn.hutool.core.util.ObjectUtil; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |||
import com.baomidou.mybatisplus.core.toolkit.Wrappers; | |||
import com.ningdatech.basic.util.StrPool; | |||
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.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 lombok.AllArgsConstructor; | |||
@@ -25,6 +35,8 @@ 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; | |||
@@ -52,6 +64,9 @@ public class ExpertCallResultRewriteTask { | |||
private final IMeetingExpertService meetingExpertService; | |||
private final IExpertInviteRuleService inviteRuleService; | |||
private final ISysMsgRecordDetailService msgRecordDetailService; | |||
private final SmsServiceClient smsServiceClient; | |||
private final MeetingExpertSmsMapper meetingExpertSmsMapper; | |||
private final static int MINUTES_CALL_RESULT_FEEDBACK = 15; | |||
private static final String AGREE_KEY = "1"; | |||
@@ -62,10 +77,65 @@ public class ExpertCallResultRewriteTask { | |||
log.warn("随机邀请已关闭……"); | |||
return; | |||
} | |||
Instant startTime = Instant.now().plus(randomInviteProperties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
// 处理电话结果回填 | |||
Instant startTime = Instant.now().plus(randomInviteProperties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
Duration fixedRate = Duration.ofMinutes(randomInviteProperties.getResultRewriteFixedRate()); | |||
scheduler.scheduleAtFixedRate(this::rewritePhoneCallResult, startTime, fixedRate); | |||
// scheduler.scheduleAtFixedRate(this::rewritePhoneCallResult, startTime, fixedRate); | |||
// 处理短信结果回填 | |||
Instant smsStartTime = Instant.now().plus(randomInviteProperties.getResultRewriteFixedRate(), ChronoUnit.MINUTES); | |||
scheduler.scheduleAtFixedRate(this::expertSmsReply, smsStartTime, fixedRate); | |||
} | |||
public void expertSmsReply() { | |||
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; | |||
} | |||
// 所有随机邀请的短信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()); | |||
// 查询随机邀请回调等待时间 | |||
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()); | |||
}); | |||
} | |||
// 获取专家回复内容 | |||
SmsReplyDetails smsReplyDetails = viewSmsReplies(smsUuids); | |||
List<MeetingExpert> updates = 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); | |||
} | |||
@@ -171,4 +241,138 @@ public class ExpertCallResultRewriteTask { | |||
return Optional.of(status.getCode()); | |||
} | |||
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; | |||
} | |||
} |
@@ -191,6 +191,7 @@ public class ExpertRandomInviteTask { | |||
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> { | |||
ExpertRandomInviteTask bean = SpringContextHolder.getBean(ExpertRandomInviteTask.class); | |||
try { | |||
// 抽取专家 | |||
bean.invite(meetingId, reInvite, tsTime); | |||
} catch (Exception e) { | |||
log.error("执行专家邀请任务异常:{}", meetingId, e); | |||
@@ -267,7 +268,9 @@ public class ExpertRandomInviteTask { | |||
expert.setStatus(ExpertAttendStatusEnum.NOTICING.getCode()); | |||
return expert; | |||
}); | |||
meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertMeetings); | |||
// meetingCallOrMsgHelper.callExpertByMeeting(meeting, expertMeetings); | |||
// 短信或电话提醒 | |||
meetingCallOrMsgHelper.smsOrCallExpertByMeeting(meeting, expertMeetings); | |||
log.info("会议:{} 后台抽取专家:{}名", meetingId, expertMeetings.size()); | |||
meetingExpertService.saveBatch(expertMeetings); | |||
} else { | |||
@@ -26,6 +26,10 @@ public class RandomInviteProperties { | |||
*/ | |||
private Integer resultRewriteFixedRate = 2; | |||
/** | |||
* 短信结果回填执行频率(分钟) | |||
*/ | |||
private Integer smsResultRewriteFixedRate = 1; | |||
/** | |||
* 随机邀请延迟执行(分钟) | |||
*/ | |||
private Integer inviteDelay = 2; | |||
@@ -16,16 +16,16 @@ public class VoiceSmsTemplateConst { | |||
/** | |||
* 短信登陆验证码 | |||
*/ | |||
public static final String SMS_VERIFY_CODE = "验证码:%s(有效期为%s分钟),请勿泄露给他人,如非本人操作,请忽略此信息。"; | |||
public static final String SMS_VERIFY_CODE = "【杭州数字信创】验证码:%s(有效期为%s分钟),请勿泄露给他人,如非本人操作,请忽略此信息。"; | |||
/** | |||
* 社会专家报名 | |||
*/ | |||
public static final String EXPERT_REGISTER = "专家报名验证码:%s(有效期为%s分钟),请勿泄露给他人,如非本人操作,请忽略此信息。"; | |||
public static final String EXPERT_REGISTER = "【杭州数字信创】专家报名验证码:%s(有效期为%s分钟),请勿泄露给他人,如非本人操作,请忽略此信息。"; | |||
/** | |||
* 专家电话通知语音模版 | |||
*/ | |||
public static final String EXPERT_INVITE = "尊敬的专家您好,%s现邀请您作为专家参加%s会议,会议时间:%s,会议地点:%s。 确认参加请按 1,拒绝参加请按 2。请您选择"; | |||
public static final String EXPERT_INVITE = "【杭州数字信创】尊敬的专家您好,%s现邀请您作为专家参加%s会议,会议时间:%s,会议地点:%s。 确认参加请按 1,拒绝参加请按 2。请您选择"; | |||
} |
@@ -2,17 +2,16 @@ package com.hz.pm.api.sms.manage; | |||
import cn.hutool.core.util.PhoneUtil; | |||
import cn.hutool.core.util.RandomUtil; | |||
import com.ningdatech.basic.exception.BizException; | |||
import com.ningdatech.cache.model.cache.CacheKey; | |||
import com.ningdatech.cache.repository.CachePlusOps; | |||
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.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.constants.YxtSmsSignEnum; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd; | |||
import com.ningdatech.yxt.model.cmd.SendSmsCmd.SendSmsContext; | |||
import lombok.RequiredArgsConstructor; | |||
@@ -70,7 +69,7 @@ public class VerificationCodeManage { | |||
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); | |||
// sendSmsCmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
} | |||
break; | |||
case EXPERT_REGISTER: { | |||
@@ -78,7 +77,7 @@ public class VerificationCodeManage { | |||
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); | |||
// sendSmsCmd.setSmsSignEnum(YxtSmsSignEnum.LS_BIG_DATA_BUREAU); | |||
} | |||
break; | |||
default: | |||