已删除1个文件
已修改11个文件
已添加1个文件
864 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/component/RedisMqReceiver.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/SmsController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceTelInfoController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/tools/BaseSmsaccountController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/HttpUtil.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/sms/SmsUtils.java 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/sms/smsUtils.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/ServiceSubtaskCountReq.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/ServiceSubtaskStatistic.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/component/RedisMqReceiver.java
@@ -16,7 +16,7 @@
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sign.Md5Utils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.service.ISysConfigService;
@@ -691,7 +691,7 @@
                    }
                    String wxCode = "";
                    if (active.equals("hzszlyy")) {
                        wxCode = smsUtils.sendChat(url, patArchive.getTelcode(), serviceSubtask.getSfzh());
                        wxCode = SmsUtils.sendChat(url, patArchive.getTelcode(), serviceSubtask.getSfzh());
                        log.info("wxCode的值为:{}", wxCode);
                    } else {
                        wxCode = getWXCode(serviceSubtask.getSfzh(), url, serviceSubtask.getTaskName(), serviceSubtask.getTaskName(), patArchive.getTelcode(), patArchive.getName(), patArchive.getPatidHis(), wxqqxx);
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/SmsController.java
@@ -3,13 +3,15 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.ruoyi.common.annotation.AddOrgId;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.dx.MessageSend;
import com.ruoyi.common.utils.HttpUtil;
import com.ruoyi.common.utils.RSAPublicKeyExample;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.ruoyi.system.service.ISysConfigService;
import com.smartor.common.MtSubmitSmUtil;
import com.smartor.common.ShiyiSmsUtil;
import com.smartor.domain.*;
@@ -71,6 +73,8 @@
    private IServiceOutPathService iServiceOutPathService;
    @Autowired
    private IServiceSubtaskService iServiceSubtaskService;
    @Autowired
    private ISysConfigService configService;
    @Value("${spring.profiles.active}")
    private String active;
@@ -82,7 +86,7 @@
    @ApiOperation("短信发送")
    @PostMapping("/sendTest")
    public AjaxResult send(@RequestBody smsVO vo) {
        String sendMsg = smsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, vo.getPhone(), vo.getContent());
        String sendMsg = SmsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, vo.getPhone(), vo.getContent());
        return AjaxResult.success(sendMsg);
    }
@@ -138,7 +142,7 @@
        String sendMsg = "false";
        try {
            if (active.equals("hzszlyy")) {
                sendMsg = smsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, vo.getPhone(), content);
                sendMsg = SmsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, vo.getPhone(), content);
            } else if (active.equals("nhfy")) {
                Map<String, Object> smsResult = mtSubmitSmUtil.submitSm(vo.getPhone(), content);
                if (smsResult != null && smsResult.get("result") != null && smsResult.get("result").toString().equals("0")) {
@@ -223,7 +227,7 @@
            serviceOutPath.setOrgid(vo.getOrgid());
            iServiceOutPathService.insertServiceOutPath(serviceOutPath);
            String url = StringUtils.isNotEmpty(req_path) ? ip + ":" + req_path + "/wt?p=" + format : ip + "/wt?p=" + format;
            sendMsg = smsUtils.sendChat(url, vo.getPhone(), vo.getIdCard());
            sendMsg = SmsUtils.sendChat(url, vo.getPhone(), vo.getIdCard());
        }
        return AjaxResult.success(sendMsg);
    }
@@ -265,4 +269,19 @@
        return AjaxResult.success(content);
    }
    /**
     * @param
     * @return
     */
    @ApiOperation("短信发送")
    @AddOrgId
    @PostMapping("/sendYunMasSms")
    public AjaxResult sendYunMasSms(@RequestBody smsVO vo) throws Exception {
//        String yunMasConfig = configService.selectConfigByKey("ls.msg.yunmas",vo.getOrgid());
        SmsUtils.YunMasConfig yunMasConfig = new SmsUtils.YunMasConfig();
        SmsUtils.SmsResult smsResult = SmsUtils.sendYunMasSms(yunMasConfig, vo.getPhone(), vo.getContent());
        return AjaxResult.success(smsResult);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceTelInfoController.java
@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.smartor;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.smartor.domain.smsVO;
import com.smartor.service.IServiceTaskoperService;
import com.smartor.service.IServiceTelInfoService;
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/tools/BaseSmsaccountController.java
@@ -11,7 +11,7 @@
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.smartor.common.MtSubmitSmUtil;
import com.smartor.domain.BaseSmsRequest;
import com.smartor.domain.BaseSmsaccount;
@@ -135,7 +135,7 @@
            try {
                JSONObject jsTemp = JSON.parseObject(baseSmsRequest.getTemplateParam());
                String content = jsTemp.getString("name");
                sendMsg = smsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, baseSmsRequest.getPhoneNumber(), content);
                sendMsg = SmsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, baseSmsRequest.getPhoneNumber(), content);
            } catch (Exception ex) {
                return AjaxResult.error(ex.getMessage());
            }
ruoyi-common/src/main/java/com/ruoyi/common/utils/HttpUtil.java
@@ -95,7 +95,26 @@
        return httpClient;
    }
    /**
     * json的post请求(只关心是否成功,非200直接抛异常)
     *
     * @param url     è¯·æ±‚url
     * @param request è¯·æ±‚报文
     * @return å›ºå®šè¿”回true(http状态非200会抛HttpRequestException)
     */
    public static Boolean postJsonRequest(String url, String request) throws HttpRequestException {
        postJsonRequestForString(url, request);
        return true;
    }
    /**
     * json的post请求,返回响应报文内容(用于需要解析返回码的场景,如云MAS短信)
     *
     * @param url     è¯·æ±‚url
     * @param request è¯·æ±‚报文
     * @return å“åº”报文内容
     */
    public static String postJsonRequestForString(String url, String request) throws HttpRequestException {
        Assert.hasLength(url, "请求url不能为空字符串。");
        EntityEnclosingMethod httpMethod = new PostMethod(url);
@@ -132,8 +151,9 @@
            String respJson = stringBuffer.toString();
            // String respJson = httpMethod.getResponseBodyAsString();
            if (resultCode == OK) {
                return true;
                return respJson;
            } else {
                log.warn("【postJsonRequestForString】请求失败,url:{},状态码:{},响应:{}", url, resultCode, respJson);
                throw new HttpRequestException(resultCode, respJson);
            }
        } catch (UnsupportedEncodingException e) {
ruoyi-common/src/main/java/com/ruoyi/common/utils/sms/SmsUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,336 @@
package com.ruoyi.common.utils.sms;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.ruoyi.common.utils.HttpUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sign.Md5Utils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * çŸ­ä¿¡å‘送工具类
 *
 * @author ruoyi
 */
@Slf4j
public final class SmsUtils {
    private SmsUtils() {
    }
    /**
     * äº‘MAS æ¯æ‰¹æ¬¡æœ€å¤šæ‰‹æœºå·æ•°é‡
     */
    private static final int YUNMAS_MAX_MOBILES = 5000;
    /**
     * äº‘MAS è¿”回码说明
     */
    private static final Map<String, String> YUNMAS_RSPCOD_DESC = new HashMap<>();
    static {
        YUNMAS_RSPCOD_DESC.put("success", "数据验证通过");
        YUNMAS_RSPCOD_DESC.put("IllegalMac", "mac校验不通过");
        YUNMAS_RSPCOD_DESC.put("IllegalSignId", "无效的签名编码");
        YUNMAS_RSPCOD_DESC.put("InvalidMessage", "非法消息,请求数据解析失败");
        YUNMAS_RSPCOD_DESC.put("InvalidUsrOrPwd", "非法用户名/密码");
        YUNMAS_RSPCOD_DESC.put("NoSignId", "未匹配到对应的签名信息");
        YUNMAS_RSPCOD_DESC.put("TooManyMobiles", "手机号数量超限(>5000)");
    }
    /**
     * äº‘MAS æŽ¥å£é…ç½®ã€‚
     * <p>
     * æ³¨æ„ï¼šsecretKey åªå‚与 mac è®¡ç®—,不能放进请求报文。
     */
    @Data
    public static class YunMasConfig {
        /**
         * æŽ¥å£åœ°å€
         */
        private String url;
        /**
         * ä¼ä¸šåç§°
         */
        private String ecName;
        /**
         * æŽ¥å£è´¦å·ç”¨æˆ·å
         */
        private String apId;
        /**
         * å¯†é’¥ï¼ˆä»…参与 mac è®¡ç®—)
         */
        private String secretKey;
        /**
         * ç­¾åç¼–码
         */
        private String sign;
        /**
         * æ‰©å±•码:精确匹配填空字符串;模糊匹配可填空字符串或自定义扩展码(与服务代码合计不超过20位)
         */
        private String addSerial;
        public String getAddSerial() {
            return addSerial == null ? "" : addSerial;
        }
        /**
         * ä¸½æ°´å¦‡å¹¼é»˜è®¤é…ç½®ï¼ˆæœªåœ¨ sys_config ä¸­é…ç½®æ—¶ä½¿ç”¨ï¼‰
         */
        public static YunMasConfig defaultLishui() {
            YunMasConfig config = new YunMasConfig();
            config.setUrl("http://9.208.5.35:1992/sms/norsubmit");
            config.setEcName("丽水市妇幼保健院");
            config.setApId("dxjkkf");
            config.setSecretKey("1q2w3e4r5t6y");
            config.setSign("D9gKbwdt7");
            config.setAddSerial("");
            return config;
        }
        /**
         * æ ¹æ®é…ç½®å†…容(json)构建,未配置的字段沿用默认值
         *
         * @param configJson å½¢å¦‚ {"url":"...","ecName":"...","apId":"...","secretKey":"...","sign":"...","addSerial":""}
         */
        public static YunMasConfig of(String configJson) {
            YunMasConfig config = defaultLishui();
            if (StringUtils.isEmpty(configJson)) {
                return config;
            }
            JSONObject json;
            try {
                json = JSON.parseObject(configJson);
            } catch (Exception e) {
                log.error("【云MAS】配置解析失败,使用默认配置,配置内容:{}", configJson, e);
                return config;
            }
            if (json == null) {
                return config;
            }
            if (StringUtils.isNotEmpty(json.getString("url"))) {
                config.setUrl(json.getString("url"));
            }
            if (StringUtils.isNotEmpty(json.getString("ecName"))) {
                config.setEcName(json.getString("ecName"));
            }
            if (StringUtils.isNotEmpty(json.getString("apId"))) {
                config.setApId(json.getString("apId"));
            }
            if (StringUtils.isNotEmpty(json.getString("secretKey"))) {
                config.setSecretKey(json.getString("secretKey"));
            }
            if (StringUtils.isNotEmpty(json.getString("sign"))) {
                config.setSign(json.getString("sign"));
            }
            if (json.containsKey("addSerial")) {
                config.setAddSerial(json.getString("addSerial"));
            }
            return config;
        }
        /**
         * å…³é”®å‚数是否齐全
         */
        public boolean isValid() {
            return StringUtils.isNotEmpty(url) && StringUtils.isNotEmpty(ecName)
                    && StringUtils.isNotEmpty(apId) && StringUtils.isNotEmpty(secretKey)
                    && StringUtils.isNotEmpty(sign);
        }
    }
    /**
     * çŸ­ä¿¡å‘送结果
     */
    @Data
    public static class SmsResult {
        /**
         * å¹³å°è¿”回的状态码(success è¡¨ç¤ºæ ¡éªŒé€šè¿‡ï¼‰
         */
        private String rspcod;
        /**
         * æ¶ˆæ¯æ‰¹æ¬¡å·ï¼Œå¤±è´¥æ—¶ä¸ºç©º
         */
        private String msgGroup;
        /**
         * å¹³å°è¿”回的数据校验结果
         */
        private boolean success;
        /**
         * æœ¬åœ°å¼‚常/失败描述,便于排查
         */
        private String message;
        /**
         * æ˜¯å¦å‘送成功(平台校验通过)
         */
        @JSONField(serialize = false, deserialize = false)
        public boolean isOk() {
            return success && "success".equals(rspcod);
        }
        public static SmsResult fail(String message) {
            SmsResult result = new SmsResult();
            result.setSuccess(false);
            result.setMessage(message);
            return result;
        }
    }
    /**
     * æ­å·žå¸‚一医院吴山院区我短信接口
     *
     * @param url      çŸ­ä¿¡URL
     * @param userName çŸ­ä¿¡å¸å·
     * @param passWord çŸ­ä¿¡å¯†ç 
     * @param phone    æ‰‹æœºå·ç»„合如 13500000001,13500000002
     * @param content  çŸ­ä¿¡å†…容
     * @return å‘送结果
     */
    public static String sendSms(String url, String userName, String passWord, String phone, String content) {
        long timestamp = new Date().getTime();
        String sign = Md5Utils.hash(userName + timestamp + Md5Utils.hash(passWord));
        content = "【杭州市肿瘤医院】" + content;
        String jsonMsg = "{" +
                "\"userName\": \"" + userName + "\"," +
                "\"content\": \"" + content + "\"," +
                "\"phoneList\": [" + phone + "]," +
                "\"timestamp\": " + timestamp + "," +
                "\"sign\": \"" + sign + "\"" +
                "}";
        Boolean sendMsg = HttpUtil.postJsonRequest(url, jsonMsg);
        return sendMsg.toString();
    }
    /**
     * æ­å·žå¸‚一医院湖滨院区我短信接口
     *
     * @param url      çŸ­ä¿¡URL
     * @param userName çŸ­ä¿¡å¸å·
     * @param passWord çŸ­ä¿¡å¯†ç 
     * @param phone    æ‰‹æœºå·ç»„合如 13500000001,13500000002
     * @param content  çŸ­ä¿¡å†…容
     * @return å‘送结果
     */
    public static String sendSms1(String url, String userName, String passWord, String phone, String content) {
        long timestamp = new Date().getTime();
        String sign = Md5Utils.hash(userName + timestamp + Md5Utils.hash(passWord));
        content = "【杭州市第一人民医院】" + content;
        String jsonMsg = "{" +
                "\"userName\": \"" + userName + "\"," +
                "\"content\": \"" + content + "\"," +
                "\"phoneList\": [" + phone + "]," +
                "\"timestamp\": " + timestamp + "," +
                "\"sign\": \"" + sign + "\"" +
                "}";
        Boolean sendMsg = HttpUtil.postJsonRequest(url, jsonMsg);
        return sendMsg.toString();
    }
    /**
     * ä¸­å›½ç§»åŠ¨äº‘MAS æ™®é€šçŸ­ä¿¡å‘送(一对一/多对一模式)
     * <p>
     * æŽ¥å£ï¼šPOST {url},报文为 json çš„ base64 ç¼–码。
     * <ul>
     *     <li>mac æŒ‰ ecName、apId、secretKey、mobiles、content、sign、addSerial é¡ºåºæ‹¼æŽ¥åŽå– MD5(32位小写)</li>
     *     <li>secretKey åªå‚与 mac è®¡ç®—,不放进请求报文</li>
     *     <li>签名由平台根据 sign ç¼–码自动追加,content ä¸­ä¸è¦å†æ‰‹å·¥æ‹¼ã€ç­¾åã€‘</li>
     * </ul>
     *
     * @param config  äº‘MAS é…ç½®
     * @param mobiles æ”¶ä¿¡æ‰‹æœºå·ï¼Œè‹±æ–‡é€—号分隔,如 13800138000,13800138001,每批不超过 5000 ä¸ª
     * @param content çŸ­ä¿¡å†…容(mac ç”¨æœ€ç»ˆå†…容计算,计算后不得再修改)
     * @return å‘送结果,通过 {@link SmsResult#isOk()} åˆ¤æ–­æ˜¯å¦æˆåŠŸ
     */
    public static SmsResult sendYunMasSms(YunMasConfig config, String mobiles, String content) {
        if (config == null || !config.isValid()) {
            return SmsResult.fail("云MAS配置不完整");
        }
        if (StringUtils.isEmpty(mobiles)) {
            return SmsResult.fail("收信手机号不能为空");
        }
        if (StringUtils.isEmpty(content)) {
            return SmsResult.fail("短信内容不能为空");
        }
        if (mobiles.split(",").length > YUNMAS_MAX_MOBILES) {
            return SmsResult.fail("手机号数量超限(>" + YUNMAS_MAX_MOBILES + ")");
        }
        String addSerial = config.getAddSerial();
        // 1. mac å¿…须用报文里最终发送的内容计算
        String mac = Md5Utils.hash(config.getEcName() + config.getApId() + config.getSecretKey()
                + mobiles + content + config.getSign() + addSerial);
        // 2. ç”¨ json ç»„装报文,避免内容中的引号/反斜杠破坏报文;secretKey ä¸è½æŠ¥æ–‡
        JSONObject body = new JSONObject(true);
        body.put("ecName", config.getEcName());
        body.put("apId", config.getApId());
        body.put("mobiles", mobiles);
        body.put("content", content);
        body.put("sign", config.getSign());
        body.put("addSerial", addSerial);
        body.put("mac", mac);
        String requestBody = Base64.getEncoder()
                .encodeToString(body.toJSONString().getBytes(StandardCharsets.UTF_8));
        try {
            String resp = HttpUtil.postJsonRequestForString(config.getUrl(), requestBody);
            log.info("【云MAS】发送短信,手机号:{},响应:{}", mobiles, resp);
            if (StringUtils.isEmpty(resp)) {
                return SmsResult.fail("云MAS响应为空");
            }
            SmsResult result = JSON.parseObject(resp, SmsResult.class);
            if (result == null) {
                return SmsResult.fail("云MAS响应解析失败:" + resp);
            }
            if (!result.isOk()) {
                String desc = YUNMAS_RSPCOD_DESC.getOrDefault(result.getRspcod(), "未知错误");
                result.setMessage(desc);
                log.error("【云MAS】发送失败,手机号:{},rspcod:{}({}),响应:{}",
                        mobiles, result.getRspcod(), desc, resp);
            }
            return result;
        } catch (Exception e) {
            log.error("【云MAS】发送异常,手机号:{},原因:{}", mobiles, e.getMessage(), e);
            return SmsResult.fail("云MAS发送异常:" + e.getMessage());
        }
    }
    /**
     * æ­å·žå¸‚一医院吴山院区微信接口
     *
     * @param url    å¾®ä¿¡URL
     * @param phone  æ‰‹æœºå· 13500000001
     * @param idCard èº«ä»½è¯
     * @return å‘送结果
     */
    public static String sendChat(String url, String phone, String idCard) {
        String urlTemp = "http://192.200.54.14:5003/sfjk/SendMessage?sfzh="
                + idCard
                + "&title=您好,邀请您填写出院调查表,请点击填写。&content=待随访&phone=" + phone
                + "&url=" + url + "&key=ff76f8904f5f32b5ee1739e8ea46e60g";
        String sendMsg = "";
        try {
            log.info("【sendChat】市一微信公众号请求参数:{}", urlTemp);
            sendMsg = HttpUtils.sendPost(urlTemp);
            log.info("【sendChat】市一微信公众号返回值:{}", sendMsg);
        } catch (Exception ex) {
            sendMsg = ex.getMessage();
            log.error(ex.getMessage());
        }
        return sendMsg;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/sms/smsUtils.java
ÎļþÒÑɾ³ý
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -17,7 +17,7 @@
import com.ruoyi.common.enums.WxGZHEnum;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.ruoyi.quartz.service.ICollectHISService;
import com.ruoyi.quartz.util.CreateDataTable;
import com.ruoyi.system.domain.SysConfig;
@@ -1003,6 +1003,24 @@
    }
    /**
     * æŒ‰æœºæž„查询配置值(同一 key åœ¨ä¸åŒæœºæž„下可配置不同值)
     *
     * @param configKey é…ç½®key
     * @param orgid     æœºæž„ID
     * @return é…ç½®å€¼ï¼Œæœªé…ç½®è¿”回 null
     */
    private String getConfigValue(String configKey, String orgid) {
        SysConfig query = new SysConfig();
        query.setConfigKey(configKey);
        query.setOrgid(orgid);
        List<SysConfig> sysConfigs = configService.selectConfigList(query);
        if (CollectionUtils.isNotEmpty(sysConfigs)) {
            return sysConfigs.get(0).getConfigValue();
        }
        return null;
    }
    /**
     * éšè®¿-补偿处理
     *
     * @param serviceSubtask
@@ -1210,33 +1228,48 @@
//                        String url = configService.selectConfigByKey("token.360", serviceSubtask.getOrgid());
//                        //丽水的短信发送方式
//                        Map<String, String> map = MsgLSEnum.getAddressByCode(serviceSubtask.getOrgid());
                        SysConfig config = new SysConfig();
                        config.setConfigKey("ls.msg.info");
                        config.setOrgid(serviceSubtask.getOrgid());
                        List<SysConfig> sysConfigs = configService.selectConfigList(config);
                        Map<String, String> msgInfo = new HashMap<>();
                        if (CollectionUtils.isNotEmpty(sysConfigs)) {
                            String configValue = sysConfigs.get(0).getConfigValue();
                        if ("47226079133110211G1001".equals(serviceSubtask.getOrgid())) {
                            // å¦‡å¹¼ä¸­å›½ç§»åŠ¨äº‘MAS,不依赖 ls.msg.info ä¸Ž token
                            SmsUtils.YunMasConfig yunMasConfig = SmsUtils.YunMasConfig.of(getConfigValue("ls.msg.yunmas", serviceSubtask.getOrgid()));
                            SmsUtils.SmsResult smsResult = SmsUtils.sendYunMasSms(yunMasConfig, sendMagParam.getPhone(), sendMagParam.getContent());
                            log.info("【sfHandlle】云MAS短信发送结果:ok={},rspcod={},msgGroup={},message={}",
                                    smsResult.isOk(), smsResult.getRspcod(), smsResult.getMsgGroup(), smsResult.getMessage());
                            if (smsResult.isOk()) {
                                isSuccess = "true";
                            } else {
                                log.error("【sfHandlle】云MAS短信发送失败,subid:{},手机号:{},原因:{}",
                                        serviceSubtask.getId(), sendMagParam.getPhone(), smsResult.getMessage());
                            }
                        } else {
                            SysConfig config = new SysConfig();
                            config.setConfigKey("ls.msg.info");
                            config.setOrgid(serviceSubtask.getOrgid());
                            List<SysConfig> sysConfigs = configService.selectConfigList(config);
                            Map<String, String> msgInfo = new HashMap<>();
                            if (CollectionUtils.isNotEmpty(sysConfigs)) {
                                String configValue = sysConfigs.get(0).getConfigValue();
                                ObjectMapper objectMapper = new ObjectMapper();
                                if (StringUtils.isNotEmpty(configValue)) {
                                    msgInfo = objectMapper.readValue(configValue, Map.class);
                                }
                            }
                            log.info("-----------map的值为:{}", msgInfo);
                            if (ObjectUtils.isEmpty(msgInfo) || StringUtils.isEmpty(msgInfo.get("address"))) {
                                setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送失败,该机构没有配置短信地址, æ—¶é—´:" + now, "5");
                                return;
                            }
                            String token = LSHospTokenUtil.getToken(serviceSubtask.getOrgid(), msgInfo.get("tokenUrl"));
                            log.info("【sfHandlle】丽水短信发送,地址:{},参数:{},机构ID:{}", msgInfo.get("address"), sendMagParam, serviceSubtask.getOrgid());
                            String dxCode = getDXCode(msgInfo.get("address"), sendMagParam.getPhone(), sendMagParam.getContent(), msgInfo.get("sendPersonId"), msgInfo.get("sendPersonName"), MsgLSEnum.getHeaderByCode(msgInfo), token);
                            log.info("【sfHandlle】丽水短信发送结果:{}", dxCode);
                            ObjectMapper objectMapper = new ObjectMapper();
                            if (StringUtils.isNotEmpty(configValue)) {
                                msgInfo = objectMapper.readValue(configValue, Map.class);
                            Map<String, Object> textParam = objectMapper.readValue(dxCode, Map.class);
                            String code = textParam.get("Code").toString();
                            if (code.equals("0")) {
                                isSuccess = "true";
                            }
                        }
                        log.info("-----------map的值为:{}", msgInfo);
                        String token = LSHospTokenUtil.getToken(serviceSubtask.getOrgid(), msgInfo.get("tokenUrl"));
                        if (ObjectUtils.isNotEmpty(msgInfo) && StringUtils.isEmpty(msgInfo.get("address"))) {
                            setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送失败,该机构没有配置短信地址, æ—¶é—´:" + now, "5");
                            return;
                        }
                        log.info("【sfHandlle】丽水短信发送,地址:{},参数:{},机构ID:{}", msgInfo.get("address"), sendMagParam, serviceSubtask.getOrgid());
                        String dxCode = getDXCode(msgInfo.get("address"), sendMagParam.getPhone(), sendMagParam.getContent(), msgInfo.get("sendPersonId"), msgInfo.get("sendPersonName"), MsgLSEnum.getHeaderByCode(msgInfo), token);
                        log.info("【sfHandlle】丽水短信发送结果:{}", dxCode);
                        ObjectMapper objectMapper = new ObjectMapper();
                        Map<String, Object> textParam = objectMapper.readValue(dxCode, Map.class);
                        String code = textParam.get("Code").toString();
                        if (code.equals("0")) {
                            isSuccess = "true";
                        }
                    } else if (active.equals("sltd") || active.equals("nhfy")) {
                        log.info("sltd进来了吗?{}   ,---sendMagParam.getContent()的参数为:{}", active, sendMagParam.getContent());
                        if (active.equals("sltd")) {
@@ -1284,13 +1317,13 @@
                        String campusid = ObjectUtils.isNotEmpty(sysDept) ? sysDept.getCampusid() : "2";
                        //湖滨单独发送方式
                        if ("1".equals(campusid)) {
                            String result = smsUtils.sendSms1(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            String result = SmsUtils.sendSms1(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            //这里需要根据result返回的值去判断,是否成功,然后将值赋值给isSuccess
                            if (StringUtils.isNotEmpty(result) && result.contains("处理成功")) {
                                isSuccess = "true";
                            }
                        } else {
                            String result = smsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            String result = SmsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            //这里需要根据result返回的值去判断,是否成功,然后将值赋值给isSuccess
                            if (StringUtils.isNotEmpty(result) && result.contains("处理成功")) {
                                isSuccess = "true";
@@ -1372,7 +1405,7 @@
                    //市一医院
                    if (active.equals("hzszlyy")) {
                        //处理中文乱码问题
                        wxCode = smsUtils.sendChat(url, patArchive.getTelcode(), serviceSubtask.getSfzh());
                        wxCode = SmsUtils.sendChat(url, patArchive.getTelcode(), serviceSubtask.getSfzh());
                    } else {
                        wxCode = getWXCode(serviceSubtask.getSfzh(), url, serviceSubtask.getTaskName(), serviceSubtask.getTaskDesc(), patArchive.getTelcode(), serviceSubtask.getSendname(), patArchive.getPatidHis(), wxqqxx);
                    }
@@ -1874,7 +1907,16 @@
    /**
     * å¤„理超时随访任务
     * @return
     * <p>
     * æ‰«æã€Œå‡ºé™¢æ»¡ deadlineDay å¤©ä»æœªå®Œæˆã€çš„首次随访任务(sendstate=7 + visit_count=1),
     * æŒ‰å½“前任务的 nexttaskid ç”Ÿæˆå†æ¬¡éšè®¿ä»»åŠ¡å¹¶è½¬åˆ°éšè®¿ä¸­å¿ƒã€‚
     * <p>
     * æ³¨æ„ï¼šæˆªæ­¢æ—¶é—´å¿…须在循环外计算一次。历史实现把 {@code deadlineDay = -deadlineDay}
     * å†™åœ¨å¾ªçŽ¯ä½“å†…ï¼Œæ¯è½®å–åä¸€æ¬¡ï¼Œå¯¼è‡´å¶æ•°è½®ç”¨ã€Œä»Šå¤© + deadlineDay」做截止时间,
     * æŠŠå‡ºé™¢ä¸æ»¡ 28 å¤©çš„超时任务也扫了进来。
     *
     * @param orgid       æœºæž„ID
     * @param deadlineDay å‡ºé™¢æ»¡å¤šå°‘天算超时(正数,如 28)
     */
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay){
        //获取任务信息
@@ -1885,24 +1927,16 @@
        st.setOrgid(orgid);
        List<ServiceTask> serviceTasks = serviceTaskMapper.selectServiceTaskList(st);
        // éšè®¿ä¸­å¿ƒç§‘室
        String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", orgid);
        SysDept sysDeptVo = new SysDept();
        SysDept centerSysDept = null;
        if(StringUtils.isNotEmpty(centerDept)){
            sysDeptVo.setDeptType("1");
            sysDeptVo.setOrgid(orgid);
            sysDeptVo.setDeptCode(centerDept);
            List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo);
            if (CollectionUtils.isNotEmpty(sysDepts)) {
                centerSysDept = sysDepts.get(0);
            }
        }
        // æˆªæ­¢æ—¶é—´åªç®—一次:出院时间 <= ä»Šå¤© - deadlineDay
        int days = Math.abs(ObjectUtils.isEmpty(deadlineDay) ? 28 : deadlineDay);
        Date deadline = DateUtils.addDays(new Date(), -days);
        log.info("【dealSubtaskDeadline】开始扫描, orgid={}, day={}, deadline={}, ä»»åŠ¡æ•°={}",
                orgid, days, deadline, serviceTasks.size());
        String config = configService.selectConfigByKey("visit.early.day");
        for (ServiceTask serviceTask : serviceTasks) {
            deadlineDay = -deadlineDay;
            Date deadline = DateUtils.addDays(new Date(), deadlineDay);
            if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) {
                continue;
            }
            //通过任务ID拿到患者信息,并且随访时间得是今天之前的
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setOrgid(orgid);
@@ -1916,46 +1950,15 @@
                    = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
            log.info("【dealSubtaskDeadline】获取到{}个超时任务, taskid={}", serviceSubtaskList.size(), serviceTask.getTaskid());
            for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
                PatMedInhosp patMedInhospVo = new PatMedInhosp();
                patMedInhospVo.setOrgid(serviceSubtask.getOrgid());
                patMedInhospVo.setInhospid(serviceSubtask.getInhospid());
                patMedInhospVo.setCry(1);
                List<PatMedInhosp> patMedInhospList = iPatMedInhospService.selectPatMedInhospList(patMedInhospVo);
                PatMedInhosp patMedInhosp = new PatMedInhosp();
                if (!patMedInhospList.isEmpty()) {
                    patMedInhosp = patMedInhospList.get(0);
                }else {
                    continue;
                }
                if(ObjectUtils.isEmpty(serviceTask.getNexttaskid())){
                    continue;
                }
                ServiceTask nextServiceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceTask.getNexttaskid());
                PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
                Integer type = 1;
                //新增再次随访任务
                ServiceSubtask serviceSubtaskAgain = iPatMedInhospService.boxedServiceSubtask(nextServiceTask,
                        patMedInhosp, patArchive, config, type);
                serviceSubtaskAgain.setId(null);
                serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
                serviceSubtaskAgain.setVisitCount(serviceSubtask.getVisitCount() + 1);
                serviceSubtaskAgain.setSendstate(2L);
                serviceSubtaskAgain.setCreateTime(new Date());
                serviceSubtaskAgain.setUpdateTime(new Date());
                serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
                if(ObjectUtils.isNotEmpty(centerSysDept)){
                    serviceSubtaskAgain.setVisitDeptCode(centerSysDept.getDeptCode());
                    serviceSubtaskAgain.setVisitDeptName(centerSysDept.getDeptName());
                }else {
                    serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
                    serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
                }
                int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain);
                if (i == 1) {
                    //再次随访任务创建成功后,原任务改为失败状态
                    serviceSubtask.setSendstate(5L);
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                try {
                    // ç”Ÿæˆé€»è¾‘统一收口到 service:锁 + å¹‚ç­‰ + äº‹åŠ¡ + å‘送方式明细初始化
                    String result = iServiceSubtaskService.addSubTaskAgainByDeadline(serviceSubtask);
                    log.info("【dealSubtaskDeadline】生成再次随访, subId={}, taskid={}, ç»“æžœ={}",
                            serviceSubtask.getId(), serviceTask.getTaskid(), result);
                } catch (Exception e) {
                    // å•条失败不影响其他患者
                    log.error("【dealSubtaskDeadline】生成再次随访失败, subId=" + serviceSubtask.getId()
                            + ", taskid=" + serviceTask.getTaskid(), e);
                }
            }
        }
smartor/src/main/java/com/smartor/domain/ServiceSubtaskCountReq.java
@@ -165,6 +165,12 @@
    private String excep;
    /**
     * å®Œæˆæ—¶çŠ¶æ€ - è¶…链接统计(1-正常完成 2-延期完成 3-超时完成)
     */
    @ApiModelProperty(value = "完成时状态:1正常完成 2延期完成 3超时完成")
    private Long finishState;
    /**
     * å½“前发送方式 - è¶…链接统计
     */
    @ApiModelProperty("当前发送方式")
smartor/src/main/java/com/smartor/domain/ServiceSubtaskStatistic.java
@@ -125,6 +125,10 @@
    @Excel(name = " é¦–次随访失败 ")
    private Long followUpFail = 0L;
    @ApiModelProperty(value = "及时随访人次(已完成且未超时,finish_state=1)")
    @Excel(name = " åŠæ—¶éšè®¿äººæ¬¡ ")
    private Long timelyFollowUp = 0L;
    @ApiModelProperty(value = "出院人次详情")
    private List<ServiceSubtask> dischargeCountInfo;
@@ -146,6 +150,9 @@
    @ApiModelProperty(value = "首次随访失败详情")
    private List<ServiceSubtask> followUpFailInfo;
    @ApiModelProperty(value = "及时随访详情")
    private List<ServiceSubtask> timelyFollowUpInfo;
    @ApiModelProperty(value = "首次随访人工详情")
    private List<ServiceSubtask> manualInfo;
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java
@@ -88,6 +88,16 @@
    public String addSubTaskAgain(ServiceSubtask ServiceSubtask);
    /**
     * é¦–次随访任务超时后,按下一任务自动生成再次随访任务(随访中心)。
     * <p>
     * ä¸Žæ‰‹åŠ¨å…¥å£çš„å·®å¼‚ï¼šéšè®¿ä¸­å¿ƒç§‘å®¤æœªé…ç½®æ—¶å›žé€€åŽŸä»»åŠ¡ç§‘å®¤ï¼›åŒä¸€åŽŸä»»åŠ¡åªç”Ÿæˆä¸€æ¡ï¼ˆé” + æ¡ä»¶æ›´æ–°å¹‚等)。
     *
     * @param serviceSubtask å·²è¶…时的首次随访子任务
     * @return ç»“果描述
     */
    public String addSubTaskAgainByDeadline(ServiceSubtask serviceSubtask);
    /**
     * ä¿®æ”¹å•一任务(随访)
     *
     * @param ServiceSubtask å•一任务(随访)
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -14,7 +14,7 @@
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
@@ -964,6 +964,136 @@
        } finally {
            unlockAgainTask(serviceSubtask.getId(), lockToken);
        }
    }
    /**
     * é¦–次随访任务超时后,按下一任务自动生成再次随访任务(随访中心)。
     * <p>
     * å®šæ—¶ä»»åŠ¡å…¥å£ {@code RyTask#dealSubtaskDeadline} ä½¿ç”¨ã€‚与手动入口一致的部分:
     * è¡¨å•/发送方式取下一任务、链路 upid + visit_count、发送方式明细初始化;
     * å·®å¼‚:随访中心科室未配置时回退原任务科室,原任务置为发送失败(sendstate=5)。
     * <p>
     * å¹‚等:同一原任务只允许生成一条再次随访(Redis é” + upid æ ¡éªŒ + åŽŸä»»åŠ¡æ¡ä»¶æ›´æ–°ï¼‰ï¼Œ
     * å¤šèŠ‚ç‚¹/多线程同时扫描时,后到的事务会在条件更新处拿到 0 è¡Œå¹¶å›žæ»šæœ¬æ¬¡æ’入。
     *
     * @param serviceSubtask å·²è¶…时的首次随访子任务
     * @return ç»“果描述
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String addSubTaskAgainByDeadline(ServiceSubtask serviceSubtask) {
        if (ObjectUtils.isEmpty(serviceSubtask) || ObjectUtils.isEmpty(serviceSubtask.getId())) {
            return "再次随访任务创建失败,原任务不存在";
        }
        String lockToken = tryLockAgainTask(serviceSubtask.getId());
        if (lockToken == null) {
            return "再次随访任务正在创建中,请勿重复提交";
        }
        try {
            // å¹‚等校验:同一原任务只允许生成一条再次随访
            ServiceSubtaskEntity againQuery = new ServiceSubtaskEntity();
            againQuery.setUpid(serviceSubtask.getId().intValue());
            if (CollectionUtils.isNotEmpty(serviceSubtaskMapper.selectServiceSubtaskList(againQuery))) {
                return "无需再次随访,该任务已经再次随访过了";
            }
            // åŽŸä»»åŠ¡å·²è¢«æ ‡è®°ä¸ºä¸å†éšè®¿ï¼ˆæ‰‹åŠ¨å…¥å£å†™è¿‡ is_visit_again=2)
            if (ObjectUtils.isNotEmpty(serviceSubtask.getIsVisitAgain()) && serviceSubtask.getIsVisitAgain() == 2) {
                return "无需再次随访,该任务已经再次随访过了";
            }
            ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
            if (ObjectUtils.isEmpty(serviceTask)) {
                return "再次随访任务创建失败,找不到对应任务 taskid = " + serviceSubtask.getTaskid();
            }
            if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) {
                return "再次随访任务创建失败,任务缺少 nexttaskid, taskid = " + serviceSubtask.getTaskid();
            }
            ServiceTask nextServiceTask = serviceTaskService.selectServiceTaskByTaskid(serviceTask.getNexttaskid());
            if (ObjectUtils.isEmpty(nextServiceTask)) {
                return "再次随访任务创建失败,找不到对应任务 nexttaskid = " + serviceTask.getNexttaskid();
            }
            PatMedInhosp patMedInhosp = selectOutHospRecord(serviceSubtask);
            if (ObjectUtils.isEmpty(patMedInhosp)) {
                return "再次随访任务创建失败,任务缺少对应的出院记录, taskid = " + serviceSubtask.getTaskid();
            }
            PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
            if (ObjectUtils.isEmpty(patArchive)) {
                return "再次随访任务创建失败,未找到患者档案, subTaskId = " + serviceSubtask.getId();
            }
            // éšè®¿ä¸­å¿ƒç§‘室:未配置(或科室不存在)时回退原任务科室,保持超时入口原有行为
            SysDept visitDept = selectCenterDept(serviceSubtask.getOrgid());
            String config = configService.selectConfigByKey("visit.early.day");
            ServiceSubtask serviceSubtaskAgain = patMedInhospService.boxedServiceSubtask(nextServiceTask,
                    patMedInhosp, patArchive, config, 1);
            if (ObjectUtils.isEmpty(serviceSubtaskAgain) || ObjectUtils.isEmpty(serviceSubtaskAgain.getTaskid())) {
                return "再次随访任务创建失败,患者出院记录不完整或下一任务配置异常, subTaskId = " + serviceSubtask.getId();
            }
            Date now = new Date();
            serviceSubtaskAgain.setId(null);
            serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
            serviceSubtaskAgain.setVisitCount(ObjectUtils.isNotEmpty(serviceSubtask.getVisitCount())
                    ? serviceSubtask.getVisitCount() + 1 : 2);
            serviceSubtaskAgain.setSendstate(2L);
            serviceSubtaskAgain.setIsVisitAgain(1);
            serviceSubtaskAgain.setCreateTime(now);
            serviceSubtaskAgain.setUpdateTime(now);
            serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
            if (ObjectUtils.isNotEmpty(visitDept)) {
                serviceSubtaskAgain.setVisitDeptCode(visitDept.getDeptCode());
                serviceSubtaskAgain.setVisitDeptName(visitDept.getDeptName());
            } else {
                serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
                serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
            }
            if (serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain) != 1) {
                return "再次随访任务创建失败,再请次重试";
            }
            // åˆå§‹åŒ–发送方式明细,补偿(compensateTask)和超时判断依赖该表
            patMedInhospService.addServiceSubtaskPreachform(serviceSubtaskAgain, null);
            // æ¡ä»¶æ›´æ–°åŽŸä»»åŠ¡ï¼š0 è¡Œè¯´æ˜Žå·²è¢«å…¶ä»–请求生成,抛异常回滚本次插入的重复数据
            if (serviceSubtaskMapper.updateIsVisitAgainById(serviceSubtask.getId(), 2) != 1) {
                throw new BaseException("无需再次随访,该任务已经再次随访过了");
            }
            // åŽŸä»»åŠ¡è¶…æ—¶ï¼Œç½®ä¸ºå‘é€å¤±è´¥
            ServiceSubtask originUpdate = new ServiceSubtask();
            originUpdate.setId(serviceSubtask.getId());
            originUpdate.setSendstate(5L);
            originUpdate.setUpdateTime(now);
            serviceSubtaskMapper.updateServiceSubtask(originUpdate);
            return "再次随访任务创建成功";
        } finally {
            unlockAgainTask(serviceSubtask.getId(), lockToken);
        }
    }
    /**
     * æŒ‰ orgid + inhospid å–出院记录(超时再次随访用)。
     */
    private PatMedInhosp selectOutHospRecord(ServiceSubtask serviceSubtask) {
        PatMedInhosp patMedInhospVo = new PatMedInhosp();
        patMedInhospVo.setOrgid(serviceSubtask.getOrgid());
        patMedInhospVo.setInhospid(serviceSubtask.getInhospid());
        patMedInhospVo.setCry(1);
        List<PatMedInhosp> patMedInhospList = patMedInhospService.selectPatMedInhospList(patMedInhospVo);
        if (CollectionUtils.isEmpty(patMedInhospList)) {
            return null;
        }
        return patMedInhospList.get(0);
    }
    /**
     * å–随访中心科室,未配置或科室不存在时返回 null(由调用方回退原任务科室)。
     */
    private SysDept selectCenterDept(String orgid) {
        String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", orgid);
        if (StringUtils.isEmpty(centerDept)) {
            return null;
        }
        SysDept sysDeptVo = new SysDept();
        sysDeptVo.setDeptType("1");
        sysDeptVo.setOrgid(orgid);
        sysDeptVo.setDeptCode(centerDept);
        List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo);
        return CollectionUtils.isEmpty(sysDepts) ? null : sysDepts.get(0);
    }
    /**
@@ -3488,6 +3618,10 @@
                if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                    serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
                }
                //及时随访人次:已完成且未超时
                if (isTimelyFollowUp(serviceSubtask)) {
                    serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
                }
                //人工已拨通总量
                if (ObjectUtils.isNotEmpty(serviceSubtask.getIsManuallyCalled())
@@ -3756,6 +3890,19 @@
    }
    /**
     * æ˜¯å¦å±žäºŽã€ŒåŠæ—¶éšè®¿ã€ï¼šéšè®¿å·²å®Œæˆä¸”完成时状态为正常完成(未超时)。
     * <p>
     * finish_state:1-正常完成 2-延期完成 3-超时完成,只有 1 è®¡å…¥åŠæ—¶éšè®¿ã€‚
     *
     * @param serviceSubtask éšè®¿å­ä»»åŠ¡
     * @return true-计入及时随访人次
     */
    private boolean isTimelyFollowUp(ServiceSubtask serviceSubtask) {
        return serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate().equals(6L)
                && serviceSubtask.getFinishState() != null && serviceSubtask.getFinishState().equals(1L);
    }
    /**
     * é¦–次随访统计
     *
     * @param serviceSubtaskList
@@ -3786,6 +3933,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //首次出院随访
@@ -3955,6 +4106,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //再次出院随访
@@ -4486,6 +4641,7 @@
     * pendingFollowUpInfo - visitCount = 1 && sendstate = 2
     * followUpSuccessInfo - visitCount = 1 && sendstate = 6
     * followUpFailInfo - visitCount = 1 && (sendstate = 5 || sendstate = 7)
     * timelyFollowUpInfo - sendstate = 6 && finish_state = 1(沿用请求的 visitCount å£å¾„)
     * manualInfo - visitCount = 1 && currentPreachForm = "1"
     * voiceInfo - visitCount = 1 && currentPreachForm = "3"
     * smsInfo - visitCount = 1 && currentPreachForm = "4"
@@ -4544,14 +4700,22 @@
    }
    private void applyHyperLinkInfoType(ServiceSubtaskCountReq serviceSubtaskCountReq, String hyperLinkInfoType) {
        //及时随访明细需要沿用请求的首次/再次口径
        Integer originVisitCount = serviceSubtaskCountReq.getVisitCount();
        serviceSubtaskCountReq.setVisitCount(null);
        serviceSubtaskCountReq.setSendstates(null);
        serviceSubtaskCountReq.setCurrentPreachform(null);
        serviceSubtaskCountReq.setExcep(null);
        serviceSubtaskCountReq.setTaskSituation(null);
        serviceSubtaskCountReq.setFinishState(null);
        switch (hyperLinkInfoType) {
            case "dischargeCountInfo":
                break;
            case "timelyFollowUpInfo":
                serviceSubtaskCountReq.setVisitCount(originVisitCount);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L)));
                serviceSubtaskCountReq.setFinishState(1L);
                break;
            case "followUpNeededAllInfo":
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 5L, 6L, 7L)));
@@ -5021,6 +5185,7 @@
        total.setPendingFollowUp(0L);
        total.setFollowUpSuccess(0L);
        total.setFollowUpFail(0L);
        total.setTimelyFollowUp(0L);
        total.setManual(0L);
        total.setSms(0L);
        total.setWeChat(0L);
@@ -5052,6 +5217,7 @@
            total.setPendingFollowUp(total.getPendingFollowUp() + (stat.getPendingFollowUp() != null ? stat.getPendingFollowUp() : 0L));
            total.setFollowUpSuccess(total.getFollowUpSuccess() + (stat.getFollowUpSuccess() != null ? stat.getFollowUpSuccess() : 0L));
            total.setFollowUpFail(total.getFollowUpFail() + (stat.getFollowUpFail() != null ? stat.getFollowUpFail() : 0L));
            total.setTimelyFollowUp(total.getTimelyFollowUp() + (stat.getTimelyFollowUp() != null ? stat.getTimelyFollowUp() : 0L));
            total.setManual(total.getManual() + (stat.getManual() != null ? stat.getManual() : 0L));
            total.setSms(total.getSms() + (stat.getSms() != null ? stat.getSms() : 0L));
            total.setWeChat(total.getWeChat() + (stat.getWeChat() != null ? stat.getWeChat() : 0L));
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -255,7 +255,7 @@
        from service_subtask
    </sql>
    <!-- å®Œæˆæ—¶çŠ¶æ€(finish_state)计算:仅当本次更新把 sendstate ç½®ä¸º 6(随访完成)时使用。
    <!-- è¶…时完成、延期完成逻辑 å®Œæˆæ—¶çŠ¶æ€(finish_state)计算:仅当本次更新把 sendstate ç½®ä¸º 6(随访完成)时使用。
         åŸºå‡†æˆªæ­¢æ—¶é—´ base = endtime + send_day(缺省1) + (Σ service_subtask_preachform.compensate_time + 1) å¤©ï¼Œ
         ä¸Ž RyTask/RedisMqReceiver çš„ isSubtaskDeadline å£å¾„保持一致;
         delay = COALESCE(service_task.delay_finish_days, 0);
@@ -1681,6 +1681,7 @@
        continue_count,
        continue_time_next,
        task_situation,
        finish_state,
        is_manually_called
        from service_subtask
        where 1=1
@@ -1970,6 +1971,7 @@
        task_situation,
        remark,
        doctor_opinion,
        finish_state,
        is_manually_called
        from service_subtask
        where 1=1
@@ -2052,6 +2054,9 @@
        <if test="taskSituation != null">
            AND task_situation = #{taskSituation}
        </if>
        <if test="finishState != null">
            AND finish_state = #{finishState}
        </if>
        <if test="drcode != null and drcode != ''">
            AND drcode = #{drcode}
        </if>