liusheng
5 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
package com.ruoyi.quartz.task;
 
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.service.IDingTalkService;
import com.ruoyi.common.enums.ServiceFromEnum;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.message.SendResult;
import com.ruoyi.message.sms.SmsSendRequest;
import com.ruoyi.message.sms.SmsSenderFactory;
import com.ruoyi.message.sms.provider.xh.XinHuaSmsSender;
import com.ruoyi.message.wechat.WechatSendRequest;
import com.ruoyi.message.wechat.WechatSenderFactory;
import com.ruoyi.quartz.constant.MzGatherConstant;
import com.ruoyi.quartz.enums.MzGatherParamEnum;
import com.ruoyi.quartz.enums.MzGatherSlotEnum;
import com.ruoyi.quartz.enums.MzGatherStatusEnum;
import com.ruoyi.quartz.service.ICollectHISService;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.message.sms.provider.sltd.ws.MessageSend;
import com.smartor.common.QwenLLMUtil;
import com.smartor.domain.*;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.smartor.mapper.*;
import com.smartor.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
 
import com.ruoyi.template.domain.HeLibrary;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
import com.ruoyi.template.api.TemplateFacade;
import com.ruoyi.common.core.domain.entity.PatArchive;
import com.ruoyi.task.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.ServiceSubtask;
import com.ruoyi.task.domain.ServiceSubtaskRecord;
import com.ruoyi.task.domain.ServiceTask;
import com.ruoyi.task.mapper.ServiceSubtaskMapper;
import com.ruoyi.task.mapper.ServiceTaskMapper;
import com.ruoyi.task.domain.ServiceSubtaskDetailTrace;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.ruoyi.task.service.IServiceSubtaskDetailTraceService;
import com.ruoyi.task.domain.ServiceOutPath;
import com.ruoyi.task.service.IServiceOutPathService;
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
import com.ruoyi.task.service.IServiceSubtaskService;
import com.ruoyi.task.domain.SendMagParam;
/**
 * 定时任务调度测试
 *
 * @author ruoyi
 */
@Slf4j
@Component("ryTask")
public class RyTask {
    @Autowired
    private IServiceThirdDataService iServiceThirdDataService;
 
    /**
     * 模板数据一律经 ruoyi-template 的门面取用(跨模块数据访问原则)。
     */
    @Autowired
    private TemplateFacade templateFacade;
 
    @Autowired
    private IServiceOutPathService iServiceOutPathService;
 
    @Autowired
    private IServiceSubtaskAnswerService serviceSubtaskAnswerService;
 
    @Autowired
    private IPatMedInhospService iPatMedInhospService;
 
    /**
     * 手术随访模块的<b>常驻门面</b>(原注入 {@code IPatMedOperationService})。
     * <p>
     * 手术随访编排已整体迁入 {@code ruoyi-othervisit},本类只做调用。
     * ⚠️ 注入门面而不是模块内部实现:门面永远存在,模块未打进应用或
     * {@code othervisit.enabled=false} 时只是降级并打 WARN,不会启动失败。
     */
    @Autowired
    private com.ruoyi.othervisit.api.OtherVisitFacade otherVisitFacade;
 
    @Autowired
    private IServiceSubtaskService iServiceSubtaskService;
 
    @Autowired
    private IServicePatientTempService iServicePatientTempService;
 
    // ⚠️ 2026-10 移除:原 `IHNGatherPatArchiveService ihnGatherPatArchiveService` 字段。
    //    它在本类里**只**被 dealHisData 的河南分支使用(死字段检查:改前全文件仅 1 处使用),
    //    该分支已随方法体搬进 ruoyi-datacollection,故字段连带删除 ——
    //    留着它会让人误以为 smartor 的河南采集还有本类这个使用方
    //    (真正的使用方已是数据采集模块经契约 HisGatherApi 调用的 Bridge)。
 
    @Autowired
    private IServiceSLTDHealthcareRecordService serviceSLTDHealthcareRecordService;
 
    @Autowired
    private SysUserImportMapper sysUserImportMapper;
 
    @Autowired
    private ICollectHISService ichService;
 
    @Autowired
    private ISysConfigService configService;
 
    @Autowired
    private IDingTalkService dingTalkService;
 
    @Autowired
    private ISysUserService userService;
 
    @Value("${localIP}")
    private String localIP;
 
    @Value("${pub_key}")
    private String pub_key;
 
    @Value("${req_path}")
    private String req_path;
 
    @Autowired
    private ServiceTaskMapper serviceTaskMapper;
 
    @Autowired
    private ServiceSubtaskMapper serviceSubtaskMapper;
 
    @Autowired
    private RedisCache redisCache;
 
    @Autowired
    private IServiceSubtaskRecordService serviceSubtaskRecordService;
 
    // ⚠️ 2026-10 移除:原 `IIcd10Service iIcd10Service` 字段。
    //    它在本类里**只**被 dealHisData 的"疾病字典增量采集"一行调用,已随方法体搬进
    //    ruoyi-datacollection(经契约 HisGatherApi#gatherIcd10 调回 smartor 的实现)。
 
    @Autowired
    IPatArchiveService patArchiveService;
 
    @Autowired
    ISysDeptService sysDeptService;
 
    @Autowired
    IServiceSubtaskDetailTraceService serviceSubtaskDetailTraceService;
 
    @Autowired
    IPatMedOuthospService patMedOuthospService;
 
    @Autowired
    IXHGatherPatArchiveService ixhGatherPatArchiveService;
 
    @Autowired
    ServiceSubtaskPreachformMapper serviceSubtaskPreachformMapper;
 
    @Autowired
    ICollectHISService icollectHis;
 
    /**
     * 门诊分表维护已整体迁入 {@code ruoyi-outpatientvisit},这里只持有它的常驻门面。
     * <p>
     * ⚠️ 注入门面而不是内部实现:门面<b>永远存在</b>,即使模块没被打进应用本类也不会启动失败。
     */
    @Autowired
    private com.ruoyi.outpatientvisit.api.OuthospVisitFacade outhospVisitFacade;
 
    /**
     * 出入院随访模块的<b>常驻门面</b>。
     * <p>
     * ⚠️ 2025 修复:本类的 <b>3 个随访入口原先不判开关</b> ——
     * 它们调用 {@code IPatMedInhospService}(smartor 里的遗留实现,见
     * {@code PatMedInhospServiceImpl}),而那个实现用门面<b>只是为了取数据</b>
     * ({@code selectPatMedInhospList} / {@code updatePatMedInhosp}),
     * <b>从不调用门面的编排方法</b>。于是 {@code inhospvisit.enabled} /
     * {@code publicity.enabled} / {@code othervisit.enabled} 对定时任务<b>完全失效</b>:
     * 门面的编排守卫({@code InhospVisitFacade#dealOutHospInfo} 等)虽然有实现、
     * 有测试,却只有 HTTP 手动触发那一条路走得到。
     * <p>
     * 现在改为与手术随访(手术随访早已走 {@code otherVisitFacade},见 {@link #dealOperationInfo()})
     * 完全同形:定时任务走门面,开关在门面里统一判。模块实现内部会回调
     * {@code PublicityApi} / {@code OtherVisitApi},那两处各自判自己的开关,
     * 所以一次切换让 3 个开关同时生效。
     * <p>
     * ⚠️ <b>不能注入 {@code InhospVisitApi} 接口</b>:容器里有两个实现
     * (本门面 + 内部实现 {@code DefaultInhospVisitApi}),按类型注入会歧义;
     * 而且只有门面才判开关。Quartz 依赖本模块、不存在反向依赖,所以直接注入门面是安全的。
     */
    @Autowired
    private com.ruoyi.inhospvisit.api.InhospVisitFacade inhospVisitFacade;
 
    /**
     * 宣教模块的<b>常驻门面</b>,用于判 {@code sys_config: publicity.enabled}。
     * <p>
     * ⚠️ 2025 修复:本类原本只在**生成**宣教时受开关管(走 inhospvisit 门面回调 PublicityApi),
     * 但**发送**宣教({@link #sfHandlle} 的 {@code type == 3} 分支)从来不读宣教开关 ——
     * 所以关掉 {@code publicity.enabled} 后,已生成的宣教子任务照样通过
     * 短信/公众号/电话发出去。
     */
    @Autowired
    private com.ruoyi.publicity.api.PublicityFacade publicityFacade;
 
    /**
     * 数据采集模块的<b>常驻门面</b>。
     * <p>
     * ⚠️ 2026-10 模块化:本类的 {@code dealHisData(String)} 方法体(连同私有帮手
     * {@code collectSegmentData})已整体搬进 {@code ruoyi-datacollection},
     * 本类只保留定时任务入口(sys_job 的 invoke_target 是 {@code ryTask.dealHisData('...')},
     * 入口名不能变)。站点分支、时间窗口、分段回溯、日志文案都在模块里,一行未改。
     * <p>
     * 注入门面而不是模块内部 Service:门面<b>永远存在</b>,且只有门面判
     * {@code sys_config: datacollection.enabled} 开关。
     */
    @Autowired
    private com.ruoyi.datacollection.api.DataCollectionFacade dataCollectionFacade;
 
    // ⚠️ 2026-10 删除:原 `@Value("${appid}") private String appid;`
    //    它是「新华微信公众号推送」的遗留字段(提交 8590ef6a 引入,只被
    //    xinHuaWXReq.setAppid(appid) 用过)。该推送逻辑已搬进 ruoyi-message
    //    (公众号凭据改由 sys_config.wx.mp.config.* 下发),本类里已无引用;
    //    但它的 @Value 没有默认值,而 application-nhfy.yml / -xh.yml / -ls.yml
    //    都没定义 appid 这个 key,这些 profile 启动时会直接报
    //    `Could not resolve placeholder 'appid'`。留着是定时炸弹,故删除。
 
    @Value("${server.port}")
    private String port;
 
    /**
     * 消息发送方工厂(短信/微信公众号)。
     * <p>
     * 原来短信与公众号的发送逻辑在本类里用 {@code if (active.equals("xx"))} 硬分支了约 300 行,
     * 现已收口到 {@code ruoyi-message} 模块,这里只负责按 {@code active} 路由。
     */
    @Autowired
    private SmsSenderFactory smsSenderFactory;
 
    @Autowired
    private WechatSenderFactory wechatSenderFactory;
 
    @Value("${spring.profiles.active}")
    private String active;
    @Autowired
    private SysConfigMapper sysConfigMapper;
 
    // ⚠️ 2025 移除:原 `IServiceTaskScheduleService serviceTaskScheduleService` 字段。
    //    它在本类里**只**被 rollTaskGroupCycle() 用来调 rollCycleTasks()(死字段检查:
    //    改前全文件仅 1 处使用)。该调用已切到 inhospVisitFacade.rollTaskGroupCycle(),
    //    所以字段连带失效并删除 —— 留着它会让人误以为 smartor 的 rollCycleTasks 还有人用。
    //    smartor 侧的 ServiceTaskScheduleServiceImpl.rollCycleTasks() 本身**保留不删**
    //    (它可能仍被现网其它入口/接口调用,删除属另一件事)。
 
    public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
        System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
    }
 
    public void ryParams(String params) {
        System.out.println("执行有参方法:" + params);
    }
 
    public void ryNoParams() {
        System.out.println("执行无参方法");
    }
 
    public void importData() {
        String filePath = "C:\\Users\\86176\\Desktop\\新建文本文档 4.txt"; // 替换为你的文件路径
 
        try {
            // 读取文件内容
            String jsonContent = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
 
            // 创建ObjectMapper对象
            ObjectMapper objectMapper = new ObjectMapper();
            // 解析JSON为JsonNode
            JsonNode rootNode = objectMapper.readTree(jsonContent);
            // 获取resultList
            JsonNode resultListNode = rootNode.path("results").path("resultList");
            // 遍历resultList中的每个对象
            for (JsonNode item : resultListNode) {
 
                SysUserImport person = objectMapper.treeToValue(item, SysUserImport.class);
                person.setStaffPhoto("");
                sysUserImportMapper.insertSysUserImport(person);
 
            }
 
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
 
    public void analysisData() {
        //数据解析
        ServiceThirdData serviceThirdData = new ServiceThirdData();
        serviceThirdData.setDataType("ADT^A03");
        serviceThirdData.setIsDeal(0);
        serviceThirdData.setCreateTime(new Date());
        iServiceThirdDataService.queryList(serviceThirdData);
    }
 
    /**
     * 定时发送SLTD满意度异常信息
     */
    public void sendSLTDMYDExceptInfo() {
        //1.获取发送人工号
        ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO();
        subtaskDetailTraceVO.setHandleFlag("0");
        List<ServiceSubtaskDetailTrace> serviceSubtaskDetailTraces = serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO);
        // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重)
        List<String> dutyDeptPersonCodeList = serviceSubtaskDetailTraces.stream().map(ServiceSubtaskDetailTrace::getDutyDeptPersonCode).filter(StringUtils::isNotBlank).flatMap(code -> Arrays.stream(code.split(","))).map(String::trim).filter(StringUtils::isNotBlank).distinct()  // 去重
                .collect(Collectors.toList());
        // 获取“省立同德满意度异常内容提醒"
        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("sltd.myd.send.except.contont");
        String exceptContent = sysConfig.getConfigValue();
 
        // 获取“省立同德满意度异常内容提醒测试人工号”
        String testPerson = sysConfigMapper.checkConfigKeyUnique("sltd.myd.send.test.person").getConfigValue();
        if (StringUtils.isNotBlank(testPerson)) {
            String[] split = testPerson.split(",");
            if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) dutyDeptPersonCodeList = Arrays.asList(split);
            else dutyDeptPersonCodeList.addAll(Arrays.asList(split));
        }
 
        if (CollectionUtils.isNotEmpty(dutyDeptPersonCodeList)) {
            for (String dutyDeptPersonCode : dutyDeptPersonCodeList) {
                log.info("【sendSLTDMYDExceptInfo】发送人工号:{}", dutyDeptPersonCode);
                String data = MessageSend.sendMsg(exceptContent, "7", dutyDeptPersonCode, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
            }
        }
    }
 
    /**
     * 定时发送异常信息-景宁
     */
    public void sendExceptInfo() throws Exception {
        //1.获取发送人工号
        ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO();
        subtaskDetailTraceVO.setHandleFlag("0");
        List<ServiceSubtaskDetailTrace> serviceSubtaskDetailTraces =
                serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO);
        // 获取“异常内容提醒"
        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("send.except.content");
        String exceptContent = sysConfig.getConfigValue();
 
        // 获取“内容提醒测试人工号”
        SysConfig testPersonConfig = sysConfigMapper.checkConfigKeyUnique("send.test.person");
        String testPerson = ObjectUtils.isNotEmpty(testPersonConfig) ? testPersonConfig.getConfigValue() : "";
        if(CollectionUtils.isNotEmpty(serviceSubtaskDetailTraces)){
            for (ServiceSubtaskDetailTrace serviceSubtaskDetailTrace : serviceSubtaskDetailTraces) {
                List<String> dutyDeptPersonCodeList = new ArrayList<>();
                        // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重)
                String dutyDeptPersonCodeStr = serviceSubtaskDetailTrace.getDutyDeptPersonCode();
                if(StringUtils.isNotEmpty(dutyDeptPersonCodeStr)){
                    String[] dutyDeptPersonCodeSplit = dutyDeptPersonCodeStr.split(",");
                    if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) {
                        dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(dutyDeptPersonCodeSplit));
                    } else {
                        dutyDeptPersonCodeList.addAll(Arrays.asList(dutyDeptPersonCodeSplit));
                    }
 
                }
 
                if (StringUtils.isNotBlank(testPerson)) {
                    String[] split = testPerson.split(",");
                    if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) {
                        dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(split));
                    } else {
                        dutyDeptPersonCodeList.addAll(Arrays.asList(split));
                    }
                }
 
                if (CollectionUtils.isNotEmpty(dutyDeptPersonCodeList)) {
                    for (String dutyDeptPersonCode : dutyDeptPersonCodeList) {
                        log.info("【sendExceptInfo】发送人工号:{}", dutyDeptPersonCode);
                        SysUser sysUser = new SysUser();
                        SysUser sysUserVo = new SysUser();
                        sysUserVo.setUserName(dutyDeptPersonCode);
                        sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid());
                        List<SysUser> users = userService.getUserList(sysUserVo);
                        String phoneNumber = "";
                        if(ObjectUtils.isNotEmpty(users)){
                            sysUser = users.get(0);
                            phoneNumber = sysUser.getPhonenumber();
                        }else {
                            continue;
                        }
 
                        if(StringUtils.isEmpty(phoneNumber)){
                            continue;
                        }
                        String dutyDeptPerson = dingTalkService.getDingTalkUserByMobile(phoneNumber);
                        if(StringUtils.isNotBlank(dutyDeptPerson)){
                            String context = TemplateUtils.render(exceptContent, serviceSubtaskDetailTrace);
                            dingTalkService.sendDingTalkText(context, dutyDeptPerson);
                            //更新状态
                            serviceSubtaskDetailTrace.setHandleFlag("1");
                            serviceSubtaskDetailTrace.setUpdateTime(new Date());
                            serviceSubtaskDetailTraceService.updateServiceSubtaskDetailTtrace(serviceSubtaskDetailTrace);
                        }
                    }
                }
            }
        }
    }
 
    /**
     * 定时处理外链的数据
     */
    public void runServiceSubTaskAnswer() {
        serviceSubtaskAnswerService.saveQuestionAnswer(null, 0L);
    }
 
    /**
     * 获取预约停诊记录信息
     *
     */
    public void getAppointRecordInfo() {
        LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay(); // 明天 00:00:00
        LocalDateTime tomorrowEnd = tomorrowStart.plusDays(1).minusSeconds(1);     // 明天 23:59:59
        ixhGatherPatArchiveService.getAppointRecordInfo("2", tomorrowStart, tomorrowEnd);
    }
 
    /**
     * 每月15号和月底,查询并处理 HIS 数据(同德,南华)
     */
    public void dealHisDataHalfMonth() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedNow = now.format(formatter);
        dealHisData(formattedNow);
    }
 
    /**
     * HIS数据采集
     * 定时任务:采集患者信息、出院、门诊数据
     * todayZeroStr  当前的时间   例如:2026-05-06 00:00:00
     * <p>
     * ⚠️ <b>2026-10 模块化</b>:方法体(连同私有帮手 {@code collectSegmentData})已整体搬进
     * {@code ruoyi-datacollection},实现见
     * {@code com.ruoyi.datacollection.service.impl.HisDataCollectionServiceImpl}。
     * 这里只保留<b>定时任务入口</b>:sys_job 的 invoke_target 仍是
     * {@code ryTask.dealHisData('...')},入口名不能变,所以本方法是<b>纯转发</b>。
     * <p>
     * 关闭数据采集模块({@code sys_config: datacollection.enabled=false})时门面会抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException};
     * 本入口捕获后只打一条 WARN —— 那是"模块被主动关掉",不是采集失败,
     * 不该让定时任务记成执行异常(否则运维会看到一片红色失败记录)。
     */
    public void dealHisData(String todayZeroStr) {
        try {
            dataCollectionFacade.dealHisData(todayZeroStr);
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【dealHisData】{}(本轮定时采集跳过)", e.getMessage());
        }
    }
 
    /**
     * 在院患者"科室 / 病区 / 床位"刷新(南华附一,每天一次)。
     * <p>
     * 在院患者会转科、转病区、换床,而 {@code pat_med_inhosp} 的位置字段只在入院时写过一次,
     * 之后不会自己更新 —— 这里每天按患者查一遍 HIS 里<b>该患者当前的在院信息</b>
     * ({@code dtcQueryHealthcareRecordList},与入院采集是同一个接口,按
     * {@code healthcareRecordNo} 单查 + {@code statusList=["FH0109.24"]}),
     * 与库内比对后只把变化的字段写回去。实现与判定口径见
     * {@code com.ruoyi.datacollection.service.impl.InhospDeptRefreshServiceImpl}。
     * <p>
     * 与 {@link #dealHisData(String)} 同样的降级口径:模块被关掉(
     * {@code sys_config: datacollection.enabled=false})时门面抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException},
     * 这里只打一条 WARN —— 那是"模块被主动关掉",不该记成定时任务执行异常。
     * <p>
     * sys_job 的 invoke_target 为 {@code ryTask.refreshInhospDeptInfo()},
     * 建议每天 03:30(避开 01:00 入院采集与 02:00 出院处理),并发策略选"禁止并发"。
     *
     * @return 本次实际更新的在院记录条数(0 = 无变化 / 站点不匹配 / 模块被关闭)
     */
    public int refreshInhospDeptInfo() {
        try {
            return dataCollectionFacade.refreshInhospDeptInfo();
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【refreshInhospDeptInfo】{}(本轮在院信息刷新跳过)", e.getMessage());
            return 0;
        }
    }
 
    /**
     * HIS省立同德门急诊数据采集(每半小时一轮,按天全量刷新 + 分页断点续采)
     * <p>
     * HIS 侧门急诊数据只能按天查,每轮拉到的都是当天全量(到晚上一万多条),这个取数成本改不了;
     * 能改的是处理成本:当天数据里绝大多数上一轮已经入库,靠"整页批量查重 + 已入库直接跳过",
     * 每轮真正需要做档案处理和入库的只有新出现的那几百条,一万多条也能在很短时间内跑完。
     * <p>
     * 每一轮做三件事:
     * <ol>
     *   <li>今天的整轮刷新:第 1 页翻到最后一页,只处理新记录;</li>
     *   <li>昨天的收尾采集:每天只做一次,兜住 23:xx 之后才写入 HIS 的记录和跨零点的数据;</li>
     *   <li>手工回溯:任务入参给了日期时,把那天补采一次(同一天只补一次,不会每轮重复补)。</li>
     * </ol>
     * 每轮最多跑 {@link MzGatherParamEnum#BUDGET_MINUTES}(默认 20 分钟),到点保存页码断点正常退出,下一轮从该页继续;
     * 每采完一页都会落一次断点,进程被杀也不会从头重扫。
     * <p>
     * 失败一律不阻断采集进度:
     * <ul>
     *   <li>数据层失败(转换/档案/入库)落 pat_med_outhosp_error 表交人工处理;</li>
     *   <li>接口层失败(网络超时、来未来返回非 200)没有数据可落表,只写 error 日志(含院区、日期、页码、接口入参),
     *       跳过该页继续往后采,本轮结束时把断点留在第一个失败页;因为每轮都是整天从头全量刷新,
     *       被跳过的页下一轮必然重新采到,不会漏;</li>
     *   <li>连续 {@link MzGatherConstant#MAX_CONSECUTIVE_PAGE_FAIL} 页接口失败判定为接口整体不可用,本轮提前结束(避免空转几十页),
     *       断点留在第一个失败页,下一轮再试。</li>
     * </ul>
     *
     * @param todayZeroStr 可选,手工指定要一次性回溯的日期,支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd;为空时只刷新今天+收尾昨天
     */
    public void dealHisDataMZ(String todayZeroStr) {
        log.info("【dealHisDataMZ】门急诊数据采集开始,入参:{}", todayZeroStr);
        // 南华附一的先不采门急诊数据
        if (!"sltd".equals(active.trim())) {
            log.info("【dealHisDataMZ】当前环境 {} 不采集门急诊数据,跳过", active.trim());
            return;
        }
 
        long budgetMinutes = getMzConfigLong(MzGatherParamEnum.BUDGET_MINUTES);
        long pageSize = getMzConfigLong(MzGatherParamEnum.PAGE_SIZE);
        long maxPage = getMzConfigLong(MzGatherParamEnum.MAX_PAGE);
 
        LocalDateTime now = LocalDateTime.now().withNano(0);
        LocalDateTime deadline = now.plusMinutes(budgetMinutes);
 
        // 获取省立同德要采集的院区
        SysConfig cf = new SysConfig();
        cf.setConfigKey("sltd.gather.campusid");
        cf.setOrgid(MzGatherConstant.ORG_ID);
        List<SysConfig> sysConfigcampusids = configService.selectConfigList(cf);
        if (CollectionUtils.isEmpty(sysConfigcampusids) || StringUtils.isBlank(sysConfigcampusids.get(0).getConfigValue())) {
            log.error("【dealHisDataMZ】SysConfig中的院区ID为空!");
            return;
        }
        List<Long> campusidList = Arrays.stream(sysConfigcampusids.get(0).getConfigValue().split(","))
                .map(String::trim).filter(StringUtils::isNotBlank).map(Long::valueOf).collect(Collectors.toList());
        log.info("【dealHisDataMZ】院区:{},单轮预算:{}分钟,每页:{}条", campusidList, budgetMinutes, pageSize);
 
        for (Long cid : campusidList) {
            if (LocalDateTime.now().isAfter(deadline)) {
                log.warn("【dealHisDataMZ】本轮时间预算已用尽,剩余院区留到下一轮继续,院区:{}", cid);
                break;
            }
            // 防止上一轮还没跑完就被下一轮叠着跑(预算配置过大、或手工触发时兜底)
            String lockKey = "mzRunning:" + cid;
            if (Boolean.TRUE.equals(redisCache.hasKey(lockKey))) {
                log.warn("【dealHisDataMZ】该院区上一轮采集尚未结束,本轮跳过,院区:{}(断点已保留,下一轮继续)", cid);
                continue;
            }
            redisCache.setCacheObject(lockKey, MzGatherConstant.MINUTE_FMT.format(now), budgetMinutes * 2, TimeUnit.MINUTES);
            try {
                collectOutpatientDaily(cid, todayZeroStr, pageSize, maxPage, deadline);
            } catch (Exception e) {
                log.error("【dealHisDataMZ】门急诊采集异常,院区:{},断点已保留,下一轮从断点继续", cid, e);
            } finally {
                redisCache.deleteObject(lockKey);
            }
        }
        log.info("【dealHisDataMZ】门急诊采集本轮结束,耗时:{}秒",
                java.time.Duration.between(now, LocalDateTime.now()).getSeconds());
    }
 
    /**
     * 采集某个院区:先补一次性回溯日期,再做昨天的收尾采集,最后整轮刷新今天
     */
    private void collectOutpatientDaily(Long cid, String backfillParam, long pageSize, long maxPage,
                                        LocalDateTime deadline) {
        String cursorKey = "mzDayCursor:" + cid;
        MzDayCursor cursor = loadMzDayCursor(cursorKey);
        LocalDate today = LocalDate.now();
        LocalDate yesterday = today.minusDays(1);
 
        // 手工入参指定的一次性回溯:同一个日期只补一次,避免定时任务每轮重复补
        LocalDateTime backfillTime = parseMzStartTime(backfillParam);
        if (backfillTime != null) {
            LocalDate backfillDay = backfillTime.toLocalDate();
            if (backfillDay.toString().equals(cursor.backfillDone)) {
                log.info("【dealHisDataMZ】入参日期 {} 已完成回溯,跳过,院区:{}", backfillDay, cid);
            } else if (backfillDay.equals(today)) {
                // 入参就是今天,交给下面的整轮刷新处理即可
                cursor.backfillDone = backfillDay.toString();
                saveMzDayCursor(cursorKey, cursor);
            } else {
                cursor.backfillDay = backfillDay.toString();
                cursor.backfillPage = 1L;
                saveMzDayCursor(cursorKey, cursor);
                log.info("【dealHisDataMZ】待回溯日期:{},院区:{}", backfillDay, cid);
            }
        }
 
        // 1) 一次性回溯
        if (cursor.backfillDay != null) {
            LocalDate day = LocalDate.parse(cursor.backfillDay);
            MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.BACKFILL, day,
                    cursor.backfillPage, pageSize, maxPage, deadline);
            if (status != MzGatherStatusEnum.DONE) {
                log.warn("【dealHisDataMZ】回溯日期 {} 本轮未完成({}),断点已保留,院区:{}", day, status.getDesc(), cid);
                return;
            }
            cursor.backfillDone = cursor.backfillDay;
            cursor.backfillDay = null;
            cursor.backfillPage = 1L;
            saveMzDayCursor(cursorKey, cursor);
        }
 
        // 2) 昨天收尾采集:每天一次,兜住 23:xx 之后才写进 HIS 的记录和跨零点的数据
        if (!yesterday.toString().equals(cursor.finalDay)) {
            MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.FINAL, yesterday,
                    cursor.finalPage, pageSize, maxPage, deadline);
            if (status != MzGatherStatusEnum.DONE) {
                log.warn("【dealHisDataMZ】昨日收尾采集 {} 本轮未完成({}),断点已保留,院区:{}", yesterday, status.getDesc(), cid);
                return;
            }
            cursor.finalDay = yesterday.toString();
            cursor.finalPage = 1L;
            saveMzDayCursor(cursorKey, cursor);
        }
 
        // 3) 今天的整轮刷新
        if (!today.toString().equals(cursor.day)) {
            cursor.day = today.toString();
            cursor.page = 1L;
            saveMzDayCursor(cursorKey, cursor);
        }
        MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.TODAY, today,
                cursor.page, pageSize, maxPage, deadline);
        if (status == MzGatherStatusEnum.DONE) {
            // 整轮刷新完成,页码归位,下一轮重新从第一页整轮刷新
            cursor.page = 1L;
            saveMzDayCursor(cursorKey, cursor);
            log.info("【dealHisDataMZ】{} 门急诊整轮刷新完成,院区:{}", today, cid);
        }
    }
 
    /**
     * 全量采集某一天(分页直到采完),每采完一页立即落一次断点
     *
     * @param slot 断点槽位(今天整轮刷新 / 昨日收尾 / 手工回溯),决定中断页码记到哪个字段
     * @return {@link MzGatherStatusEnum} 采集完成 / 预算用尽 / 接口连续失败提前结束
     */
    private MzGatherStatusEnum runDayPass(Long cid, String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot,
                                          LocalDate day, long startPage, long pageSize, long maxPage,
                                          LocalDateTime deadline) {
        String startStr = day.format(MzGatherConstant.DAY_FMT);
        String endStr = day.plusDays(1).format(MzGatherConstant.DAY_FMT);
 
        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId(MzGatherConstant.ORG_ID);
        reqVO.setCampusId(cid);
        reqVO.setStartHeadTime(null);
        reqVO.setStartTailTime(null);
        reqVO.setStatusList(null);
        // 接口只支持按天查,起止都传日期
        reqVO.setEncounterTimeStart(startStr);
        reqVO.setEncounterTimeEnd(endStr);
        List<String> typeList = new ArrayList<>();
        typeList.add("FH0108.01"); // 门诊
        typeList.add("FH0108.03"); // 急诊
        reqVO.setHealthcareRecordTypeList(typeList);
        reqVO.setSize(pageSize);
 
        long page = startPage < 1L ? 1L : startPage;
        long total = 0L;
        long pageCount = 0L;
        // 接口层失败跳过的页数、第一个失败页、连续失败页数
        long skippedPages = 0L;
        long firstFailPage = -1L;
        int consecutiveFail = 0;
        while (true) {
            if (LocalDateTime.now().isAfter(deadline)) {
                // 本轮有跳过的页时,断点回到第一个失败页,保证下一轮先把这些页补回来
                long resumePage = (skippedPages > 0 && firstFailPage > 0) ? firstFailPage : page;
                saveMzDayPage(cursorKey, cursor, slot, resumePage);
                log.warn("【dealHisDataMZ】本轮时间预算用尽,{} 本轮已采{}页{}条{},下次从第{}页继续,院区:{}",
                        day, pageCount, total, skippedLog(skippedPages), resumePage, cid);
                return MzGatherStatusEnum.BUDGET_UP;
            }
            reqVO.setCurrent(page);
            log.info("【dealHisDataMZ】采集门急诊数据,院区:{},日期:{},第{}页", cid, day, page);
 
            List<ServiceSLTDInhospResDTO> pageData = null;
            Exception lastError = null;
            for (int attempt = 1; attempt <= MzGatherConstant.PAGE_RETRY_TIMES; attempt++) {
                try {
                    pageData = serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
                    lastError = null;
                    break;
                } catch (Exception e) {
                    lastError = e;
                    log.error("【dealHisDataMZ】门急诊第{}页第{}次采集失败,院区:{},日期:{}", page, attempt, cid, day, e);
                    if (attempt < MzGatherConstant.PAGE_RETRY_TIMES) {
                        try {
                            Thread.sleep(MzGatherConstant.PAGE_RETRY_SLEEP_MS);
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
            if (lastError != null) {
                // 接口层失败(网络超时 / 来未来返回非200)没有数据可以落表,只写 error 日志,然后跳过该页继续往后采,不阻断进度。
                // 每轮都是整天从头全量刷新,被跳过的页下一轮必然重新采到,所以不会漏数据。
                skippedPages++;
                if (firstFailPage < 0) {
                    firstFailPage = page;
                }
                consecutiveFail++;
                log.error("【dealHisDataMZ】接口层采集失败,已记 error 日志并跳过该页,本轮继续往后采;"
                                + "院区:{},日期:{},第{}页,接口入参:{},异常:{}",
                        cid, day, page, new Gson().toJson(reqVO), lastError.getMessage(), lastError);
                if (consecutiveFail >= MzGatherConstant.MAX_CONSECUTIVE_PAGE_FAIL) {
                    // 连续多页接口失败,判定来未来接口整体不可用,本轮提前结束(不再空转后面几十页),断点留在第一个失败页
                    log.error("【dealHisDataMZ】连续{}页接口失败,判定来未来接口不可用,本轮提前结束;院区:{},日期:{},断点保留在第{}页",
                            consecutiveFail, cid, day, firstFailPage);
                    saveMzDayPage(cursorKey, cursor, slot, firstFailPage);
                    return MzGatherStatusEnum.FAILED;
                }
                page++;
                continue;
            }
            consecutiveFail = 0;
 
            if (CollectionUtils.isEmpty(pageData)) {
                log.info("【dealHisDataMZ】{} 第{}页无数据,采集结束,院区:{},本轮{}页共{}条{}",
                        day, page, cid, pageCount, total, skippedLog(skippedPages));
                return MzGatherStatusEnum.DONE;
            }
            pageCount++;
            total += pageData.size();
            log.info("【dealHisDataMZ】{} 第{}页采集{}条,院区:{}", day, page, pageData.size(), cid);
            if (pageData.size() < pageSize) {
                log.info("【dealHisDataMZ】{} 采集结束,院区:{},本轮{}页共{}条{}", day, cid, pageCount, total, skippedLog(skippedPages));
                return MzGatherStatusEnum.DONE;
            }
            page++;
            if (page > maxPage) {
                log.error("【dealHisDataMZ】{} 已翻到第{}页,超过单日上限{}页,按采完处理,院区:{}", day, page, maxPage, cid);
                return MzGatherStatusEnum.DONE;
            }
            // 每采完一页立即落断点:进程被重启/强杀也能从这一页继续,不从头重扫,也不会漏
            saveMzDayPage(cursorKey, cursor, slot, page);
        }
    }
 
    /** 按槽位保存中断页码 */
    private void saveMzDayPage(String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, long page) {
        if (slot == MzGatherSlotEnum.FINAL) {
            cursor.finalPage = page;
        } else if (slot == MzGatherSlotEnum.BACKFILL) {
            cursor.backfillPage = page;
        } else {
            cursor.page = page;
        }
        saveMzDayCursor(cursorKey, cursor);
    }
 
    /** 日志里提示接口层失败跳过了几页(下一轮整轮刷新会重新采到) */
    private String skippedLog(long skippedPages) {
        return skippedPages > 0L ? ",接口失败跳过" + skippedPages + "页(下一轮整轮刷新会重新采到)" : "";
    }
 
    /** 读取 sys_config 里的门急诊采集参数(机构 orgid=20001001),取不到或超范围时用枚举里的默认值 */
    private long getMzConfigLong(MzGatherParamEnum param) {
        try {
            SysConfig config = new SysConfig();
            config.setConfigKey(param.getKey());
            config.setOrgid(MzGatherConstant.ORG_ID);
            List<SysConfig> configs = configService.selectConfigList(config);
            if (CollectionUtils.isNotEmpty(configs) && StringUtils.isNotBlank(configs.get(0).getConfigValue())) {
                long value = Long.parseLong(configs.get(0).getConfigValue().trim());
                if (value >= param.getMin() && value <= param.getMax()) {
                    return value;
                }
                log.warn("【dealHisDataMZ】配置{}的值{}超出范围[{},{}],使用默认值{}",
                        param.getKey(), value, param.getMin(), param.getMax(), param.getDefaultValue());
            }
        } catch (Exception e) {
            log.warn("【dealHisDataMZ】读取配置{}失败,使用默认值{}", param.getKey(), param.getDefaultValue(), e);
        }
        return param.getDefaultValue();
    }
 
    /** 解析任务入参(要一次性回溯的日期),支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd */
    private LocalDateTime parseMzStartTime(String text) {
        LocalDateTime time = parseMzTimeQuiet(text);
        if (time == null && StringUtils.isNotBlank(text)) {
            log.warn("【dealHisDataMZ】入参 {} 无法解析为日期,按未指定处理", text);
        }
        return time;
    }
 
    /** 静默解析日期,支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd,格式不符返回 null(不刷日志) */
    private LocalDateTime parseMzTimeQuiet(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        try {
            return LocalDateTime.parse(value, MzGatherConstant.MINUTE_FMT);
        } catch (Exception ignore) {
            // 兼容只传日期的情况,落到下面按日期解析
        }
        try {
            return LocalDate.parse(value).atStartOfDay();
        } catch (Exception ignore) {
            return null;
        }
    }
 
    /** 保存门急诊采集断点(各轮次中断的页码) */
    private void saveMzDayCursor(String cursorKey, MzDayCursor cursor) {
        JSONObject json = new JSONObject();
        json.put("day", cursor.day);
        json.put("page", cursor.page);
        json.put("finalDay", cursor.finalDay);
        json.put("finalPage", cursor.finalPage);
        json.put("backfillDay", cursor.backfillDay);
        json.put("backfillPage", cursor.backfillPage);
        json.put("backfillDone", cursor.backfillDone);
        json.put("updateTime", MzGatherConstant.MINUTE_FMT.format(LocalDateTime.now().withNano(0)));
        redisCache.setCacheObject(cursorKey, json.toJSONString(), MzGatherConstant.CACHE_TTL_HOURS, TimeUnit.HOURS);
    }
 
    /** 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新) */
    private MzDayCursor loadMzDayCursor(String cursorKey) {
        MzDayCursor cursor = new MzDayCursor();
        try {
            String json = redisCache.getCacheObject(cursorKey);
            if (StringUtils.isBlank(json)) {
                return cursor;
            }
            JSONObject obj = JSONObject.parseObject(json);
            cursor.day = obj.getString("day");
            cursor.page = obj.getLongValue("page");
            cursor.finalDay = obj.getString("finalDay");
            cursor.finalPage = obj.getLongValue("finalPage");
            cursor.backfillDay = obj.getString("backfillDay");
            cursor.backfillPage = obj.getLongValue("backfillPage");
            cursor.backfillDone = obj.getString("backfillDone");
            if (cursor.page < 1L) {
                cursor.page = 1L;
            }
            if (cursor.finalPage < 1L) {
                cursor.finalPage = 1L;
            }
            if (cursor.backfillPage < 1L) {
                cursor.backfillPage = 1L;
            }
            return cursor;
        } catch (Exception e) {
            log.warn("【dealHisDataMZ】读取断点{}失败,按空断点处理", cursorKey, e);
            return new MzDayCursor();
        }
    }
 
    /**
     * 门急诊采集断点:每天一轮全量刷新,这里分别记录三个轮次中断在第几页
     * (页码为 1 表示该轮次下一轮从第一页整轮开始)
     */
    private static class MzDayCursor {
        /** 今天的整轮刷新 */
        private String day;
        private long page = 1L;
        /** 昨天的收尾采集 */
        private String finalDay;
        private long finalPage = 1L;
        /** 待做的一次性回溯 */
        private String backfillDay;
        private long backfillPage = 1L;
        /** 已经补过的回溯日期,避免定时任务每轮重复补同一天 */
        private String backfillDone;
    }
 
    /**
     * 同德药品数据采集
     */
 
    public void dealHisDataMedication() {
        final long OUTP_PAGE_SIZE = 500L;
        ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO();
        serviceSLTDInhospReqVO.setOrgId("20001001");
        Long outpPage = 1L;
        int totalCount = 0;  // 统计总共采集的药品数量
 
        while (true) {
            serviceSLTDInhospReqVO.setCurrent(outpPage);
            serviceSLTDInhospReqVO.setSize(OUTP_PAGE_SIZE);
            log.info("【dealHisData】省立同德\"药品数据\"采集第{}页数据", outpPage);
 
            try {
                Integer fetchedCount = serviceSLTDHealthcareRecordService.queryMedicationItemList(serviceSLTDInhospReqVO);
                log.info("【dealHisData】省立同德\"药品数据\"第{}页采集到{}条记录", outpPage, fetchedCount);
 
                totalCount += fetchedCount != null ? fetchedCount : 0;
 
                // 如果返回的数据量小于 pageSize,说明已经是最后一页了,退出循环
                if (fetchedCount == null || fetchedCount < OUTP_PAGE_SIZE) {
                    log.info("【dealHisData】省立同德\"药品数据\"采集完成,总页数:{}, 总计{}条记录", outpPage, totalCount);
                    break;
                }
 
                outpPage++;
            } catch (Exception e) {
                log.error("【dealHisData】省立同德\"药品数据\"第{}页采集失败", outpPage, e);
                // 出错时也可以选择退出或继续,这里选择退出避免无限重试
                break;
            }
        }
    }
 
    public void dealHisDataSLTD() {
        log.info("【dealHisData】开始执行HIS数据采集任务");
 
        try {
            LocalDate startDate = LocalDate.of(2025, 11, 1);
            LocalDate endDate = LocalDate.of(2026, 2, 27);
 
            // 省立同德数据采集
//            String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
                String nowTime = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO();
                serviceSLTDInhospReqVO.setOrgId("20001001");
                serviceSLTDInhospReqVO.setCampusId(30001002L);
                log.info("【dealHisData】省立同德“门急诊”开始采集数据,院区:{}", "30001002");
                serviceSLTDInhospReqVO.setStartHeadTime(null);
                serviceSLTDInhospReqVO.setStartTailTime(null);
                serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime);
                serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime);
                serviceSLTDInhospReqVO.setStatusList(null);
                List<String> list1 = new ArrayList<>();
                list1.add("FH0108.01");
                list1.add("FH0108.03");
                serviceSLTDInhospReqVO.setHealthcareRecordTypeList(list1);
                log.info("【dealHisData】省立同德“门急诊”采集数据入参:{}", serviceSLTDInhospReqVO);
                serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                log.info("【dealHisData】省立同德“门急诊”束采集数据,院区:{}", "30001002");
            }
        } catch (Exception e) {
            log.error("【dealHisData】省立同德数据采集异常", e);
        }
 
    }
 
    /**
     * 这个只是新华临时使用,平时用不到
     */
    public void dealHisData2() {
        //将患者信息、出入院数据全部采集
        // 指定的开始日期
        LocalDate endDate = LocalDate.of(2026, 7, 6);
        // 当前日期
        LocalDate currentDate = LocalDate.now();
        // 定义日期格式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd ");
 
        // 从开始日期到当前日期逐天打印
        for (LocalDate date = endDate; !date.isAfter(currentDate); date = date.plusDays(1)) {
            System.out.println(date.format(formatter));
            ixhGatherPatArchiveService.getInHospDataGather("0", date.minusDays(1).atStartOfDay(), date.atStartOfDay());
            ixhGatherPatArchiveService.getInHospDataGather("1", date.minusDays(1).atStartOfDay(), date.atStartOfDay());
        }
 
    }
 
    /**
     * 处理患者信息,进入子任务表
     * <p>
     * ⚠️ 2025 修复(开关失效):本方法原走 {@code iPatMedInhospService.dealOutHospInfo(config)},
     * 那是 smartor 里的遗留实现,**不判任何模块开关** —— 于是:
     * <ul>
     *   <li>关 {@code inhospvisit.enabled} 照样生成出院/长期随访;</li>
     *   <li>关 {@code publicity.enabled} 照样发入院宣教与药品宣教;</li>
     *   <li>关 {@code othervisit.enabled} 照样生成手术随访。</li>
     * </ul>
     * 现改走模块门面:门面判 {@code inhospvisit.enabled},模块实现内部再回调
     * {@code PublicityApi} / {@code OtherVisitApi},三处各自判自己的开关。
     */
    public void dealOutHospInfo() {
        String config = configService.selectConfigByKey("visit.early.day");
        if (port.equals("8095")) {
            //出院表(含入院宣教 / 药品宣教 / 手术随访 —— 均由模块实现回调对应模块门面)
            inhospVisitFacade.dealOutHospInfo(config);
            //门诊表(新华的暂时不做门诊随访)
            if (!active.equals("xh")) {
                // 【门急诊模块化切换】门面内部判 sys_config: outpatientvisit.enabled(运行时热读),
                // 关闭时返回 0 并打 WARN。
                //
                // ⚠️ 2025 修复:这里原先是 iPatMedOuthospService.dealOutpatientInfo(),
                // 它经 IPatMedOuthospService → PatMedOuthospServiceDelegatingImpl
                // → OuthospDataFacade.dealOutpatientInfo(),而**那条路上没有开关守卫** ——
                // OuthospVisitFacade.dealOutpatientInfo() 才是带守卫的入口,却全仓零调用者。
                // 结果是 outpatientvisit.enabled=false 对定时任务**完全失效**。
                // 现在与下面 dealOperationInfo() 走 otherVisitFacade 的形状一致。
                outhospVisitFacade.dealOutpatientInfo();
            }
        }
    }
 
    /**
     * 处理患者信息,进入子任务表(任务组)
     * <p>
     * ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 {@link #dealOutHospInfo()})。
     * 现走模块门面。注意模块侧的 {@code dealOutHospInfoBySchedule} 内部仍会转发回 smartor
     * ({@code OtherVisitApiBridge},见该类注释),但<b>开关在门面这一层判</b>,所以关闭时不会走到那里。
     */
    public void dealOutHospInfoBySchedule() {
        String config = configService.selectConfigByKey("visit.early.day");
        if (port.equals("8095")) {
            //出院表
            inhospVisitFacade.dealOutHospInfoBySchedule(config);
            //门诊表(新华的暂时不做门诊随访)
//            if (!active.equals("xh")) {
//                //iPatMedOuthospService.dealOutpatientInfo();
//            }
        }
    }
 
    /**
     * 处理手术信息,进入子任务表
     */
    public void dealOperationInfo() {
        // 【手术模块化切换】出口改为调用 ruoyi-othervisit 的常驻门面。
        // 门面内部判 sys_config: othervisit.enabled(运行时热读),关闭时返回 0 并打 WARN。
        if (port.equals("8095")) otherVisitFacade.dealOperationInfo();
 
    }
 
    /**
     * 门诊分表定时任务:从 sys_config 读取分表模式,按模式建表并刷新视图 v_pat_med_outhosp
     * 配置 key: outhosp.split.mode
     * quarter -> 按季度,表名示例:pat_med_outhosp_20260103
     * year    -> 按年,表名示例:pat_med_outhosp_2026
     * month   -> 按月,表名示例:pat_med_outhosp_202601
     * 查询不到配置则不分表
     */
    public void createOuthospQuarterTable() {
        log.info("【createOuthospQuarterTable】开始执行门诊分表任务");
        // 【门诊模块化】CreateDataTable 已整体迁入 ruoyi-outpatientvisit
        // (实现为 OuthospPartitionMaintainer,经 OuthospVisitFacade 暴露)。
        // 这里只做调用;建表逻辑一行未动。
        outhospVisitFacade.createOuthospQuarterTable();
    }
 
    /**
     * 处理患者信息,进入子任务表(微信小程序)
     */
    public void dealOutHospInfoXHC() {
        if (port.equals("8095")) {
            PatMedInhosp patMedInhosp = new PatMedInhosp();
            //获取未处理的数据
            List<ServicePatientTemp> servicePatientTemps = iServicePatientTempService.selectServicePatientTempList(new ServicePatientTemp());
        }
    }
 
    /**
     * 长期任务发送
     */
    public void longTaskSend() {
        //获取任务信息
        ServiceTask st = new ServiceTask();
        st.setDelFlag("0");
        st.setLongTask(1);
        List<ServiceTask> serviceTasks = serviceTaskMapper.selectServiceTaskList(st);
        log.info("【longTaskSend】获取到{}个长期任务", serviceTasks.size());
        for (ServiceTask serviceTask : serviceTasks) {
            CommonTaskcallMQ commonTaskcallMQ = new CommonTaskcallMQ();
            commonTaskcallMQ.setTaskid(serviceTask.getTaskid());
            commonTaskcallMQ.setPreachform(serviceTask.getPreachform());
            commonTaskcallMQ.setSendType("2");
 
            //通过任务ID拿到患者信息,并且随访时间得是今天之前的
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setTaskid(commonTaskcallMQ.getTaskid());
            serviceSubtaskVO.setSendstate(2L);
            serviceSubtaskVO.setIsVisitAgain(1);
            serviceSubtaskVO.setVisitTime(new Date());
            List<ServiceSubtask> selectServiceSubtaskList = serviceSubtaskMapper.queryServiceSubtaskList(serviceSubtaskVO);
            for (ServiceSubtask serviceSubtask : selectServiceSubtaskList) {
                sfHandlle(serviceSubtask);
            }
        }
    }
 
    /**
     * 处理补偿任务
     */
    public void compensateTask() {
        //获取到sendstate=3、1 并且 visit_time为小于等于今天的subtask
        List<Long> list = Arrays.asList(3L, 1L);
        ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
        List<ServiceSubtask> serviceSubtaskList = new ArrayList<>();
        for (Long sendstate : list) {
            serviceSubtaskVO.setSendstate(sendstate);
            serviceSubtaskVO.setVisitTime(new Date());
            List<ServiceSubtask> compensateServiceSubtaskList = serviceSubtaskMapper.getCompensateServiceSubtaskList(serviceSubtaskVO);
            serviceSubtaskList.addAll(compensateServiceSubtaskList);
        }
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            //根据当前的执行方式,获取下一种执行方式
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
            serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
            serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
            serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid());
            List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
            //获取当前执行方式的序号
            Optional<Long> currentSort = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getSort).findFirst();
            Optional<Long> id = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getId).findFirst();
            if (currentSort.isPresent()) {
                //1先检查一下,是不是有执行状态是完成的(怕之前已经有完的了,没有将servuce_subtask的状态改成功,这里再检查一下)
                boolean finishState = serviceSubtaskPreachforms.stream().allMatch(item -> item.getSendstate().equals("9"));
                if (finishState) {
                    serviceSubtask.setSendstate(6L);
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                    continue;
                }
 
                //2判断一下,当前的sort是不是等于需要执行的总个数,如果等于的话,说明是最后一个,直接将servuce_subtask的状态改成5,执行失败就行了
                Long cs = currentSort.get();
 
                if (cs.equals(serviceSubtaskPreachforms.size())
                        && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                    serviceSubtask.setSendstate(7L);
                    serviceSubtask.setRemark("处理补偿任务,当前处理最后补偿,全部执行失败(超时)");
 
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                    //修改发送方式的状态为失败
                    serviceSubtaskPreachform.setSendstate("5");
                    serviceSubtaskPreachform.setId(id.get());
 
                    serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform);
                    continue;
                }
 
                //3.不是最后一个,获取到下一个执行方式(因为都是在今天执行,那就直接发出去就完了)
                sfHandlle(serviceSubtask);
            }
        }
    }
 
    /**
     * 设置失败任务默认值,并将失败任务重新置为成功
     * <p>
     * *@param failDay   (失败天数:距离当前日期失败天数)
     *
     * @return
     */
    public void setFailSubTaskDefalutVal() {
        String failDay = configService.selectConfigByKey("hosp.default.fail.day");
        if (StringUtils.isNotEmpty(failDay)) iServiceSubtaskService.setFailSubTaskDefalutVal(Long.valueOf(failDay));
    }
 
    /**
     * 判断日期是否是今天
     *
     * @param date
     * @return
     */
    private boolean isDateToday(Date date) {
        if (date == null) {
            return false;
        }
        // 将 Date 转为 LocalDate
        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        // 获取今天的 LocalDate
        LocalDate today = LocalDate.now();
        // 如果日期早于或等于今天,返回 true
        return !localDate.isAfter(today);
    }
 
    /**
     * 随访-补偿处理
     *
     * @param serviceSubtask
     */
    private void sfHandlle(ServiceSubtask serviceSubtask) {
        //这里需要根据身份证去重(给微信小程序的固定任务用的)
        List<ServiceSubtask> serviceSubtaskListFilter = new ArrayList<>();
        ServiceTask ivrTask1 = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
        serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
        serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
        //获取发送方式
        String sendPreachform = getSendPreachform(serviceSubtaskPreachform, serviceSubtask);
        if (sendPreachform.equals("-1") || serviceSubtask.getSendstate() == 6 || serviceSubtask.getSendstate() == 5 || serviceSubtask.getSendstate() == 4) {
            //说明已经全部遍历完了,当前的执行方式序号是最后一个了   或者  该患者已经随访结束(成功或失败了或不执行)
            log.error("说明已经全部遍历完了,当前的执行方式序号是最后一个了   或者  该患者已经随访结束(成功或失败了或不执行)");
            return;
        }
        //先判断患者是不是还要发送
        PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
        if (!Objects.isNull(patArchive) && patArchive.getNotrequiredFlag() == "1") {
            //不需要发送
            serviceSubtask.setResult("患者不需要随访");
            serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "患者不需要随访," + patArchive.getNotrequiredreason() : "患者不需要随访," + patArchive.getNotrequiredreason());
 
            serviceSubtask.setSendstate(4L);
            iServiceSubtaskService.insertServiceSubtask(serviceSubtask);
            setSuccessPreachForm(serviceSubtask, "-1", "不需要执行", "6");
            return;
        }
 
        String descByCode = ServiceFromEnum.getDescByCode(Integer.valueOf(sendPreachform));
 
        // ⚠️ 2025 修复(宣教开关失效):type == 3 是**宣教/通知**(纯发送、无作答 ——
        // 见 setSuccessPreachForm 里「宣教或通知…直接改成 9」、以及本方法后面
        // 「宣教或通知,发送成功则直接改成 6L」的判定),整条派发的所有通道
        // (电话/短信/公众号/人工)发的都是宣教内容。关掉 publicity.enabled 时应**一个通道都不发**。
        //
        // ⚠️ 刻意**不推进 subtask 状态、也不记失败**(与 RedisMqReceiver 同口径):
        // 关闭是"暂时不发",不是"这个患者不需要宣教"。标成失败/不需要会让这条宣教
        // 在开关恢复后永远丢失。保持 sendstate 不动,开关打开后下一次 longTaskSend/compensateTask
        // 会照常把它捞出来发出。
        if ("3".equals(serviceSubtask.getType()) && !publicityFacade.isEnabled()) {
            log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),跳过宣教发送。subtaskId={}, patid={}",
                    serviceSubtask.getId(), serviceSubtask.getPatid());
            return;
        }
 
        if (descByCode.equals("电话")) {
            List<String> list = new ArrayList<>();
            ServiceTask ivrTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
            if (ivrTask.getSendState() != null && ivrTask.getSendState() == 3 || ivrTask.getSendState() != null && ivrTask.getSendState() == 4) {
                //如何任务被"暂停"或"终止"
                return;
            }
 
            if (org.apache.commons.lang3.StringUtils.isEmpty(serviceSubtask.getPhone())) {
                setSuccessPreachForm(serviceSubtask, sendPreachform, "手机号为空", "4");
                return;
            }
            boolean dateToday = isDateToday(serviceSubtask.getVisitTime());
            //说明是立即发送(先把消息放到cache-0中,   displayNo不知道怎么配置)
            if (dateToday) {
                list.add(serviceSubtask.getId().toString());
                //在子任务表里记录一下
                setSuccessPreachForm(serviceSubtask, sendPreachform, "长期任务,电话进入队列成功", "2");
                //任务发送记录
                ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
                serviceSubtaskRecord.setSubtaskId(serviceSubtask.getId());
                serviceSubtaskRecord.setUuid(UUID.randomUUID().toString());
                serviceSubtaskRecord.setTasktype(serviceSubtask.getType());
                serviceSubtaskRecord.setPreachform("3");
                serviceSubtaskRecord.setStartTime(System.currentTimeMillis());
                serviceSubtaskRecord.setRemark("长期任务,电话进入队列成功");
                serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
            }
            //将要执行的放到缓存中
            if (list.size() > 0) {
                redisCache.setCacheListLeftAndDistinct("cache-0", list);
                log.info("【sfHandlle】电话任务已加入队列,数量:{}", list.size());
            }
        } else if (descByCode.equals("微信小程序")) {
            //微信小程序
            if (ObjectUtils.isNotEmpty(ivrTask1) && ivrTask1.getPatCycle() == 1) {
                //说明该任务的患者是循环执行的
                //判断患者过滤集合中,是否存在当前执行患者的身份证
                boolean continutFlag = false;
                for (ServiceSubtask ss : serviceSubtaskListFilter) {
                    if (ss.getSfzh().equals(serviceSubtask.getSfzh())) {
                        continutFlag = true;
                    }
                }
                //如果存在当前执行患者的身份证,则不执行该患者了
                if (continutFlag) {
                    return;
                }
 
                //将子任务ID删除 、sendstate改成3(循环任务就是这样)
                serviceSubtask.setSendstate(3L);
                serviceSubtask.setId(null);
                serviceSubtask.setFinishtime(null);
                serviceSubtask.setCreateTime(new Date());
                serviceSubtask.setUpdateTime(new Date());
                serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
 
                //将当前要执行的患者放到过滤患者集合中
                serviceSubtaskListFilter.add(serviceSubtask);
            }
        } else if (descByCode.equals("短信")) {
            RSAPublicKeyExample rsaPublicKeyExample = new RSAPublicKeyExample();
            String taskId = rsaPublicKeyExample.encryptedData(ivrTask1.getTaskid().toString(), pub_key);
            log.info("【sfHandlle】处理短信长期任务,患者ID:{}", serviceSubtask.getPatid());
            boolean dateToday = isDateToday(serviceSubtask.getVisitTime());
            //记录当前时间 如果失败记录当前时间
            LocalDateTime now = LocalDateTime.now();
            if (dateToday) {
                try {
                    String patid = rsaPublicKeyExample.encryptedData(serviceSubtask.getPatid().toString(), pub_key);
                    String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key);
                    SendMagParam sendMagParam = new SendMagParam();
                    sendMagParam.setType("4");
                    ServiceOutPath serviceOutPath = new ServiceOutPath();
                    serviceOutPath.setParam1(taskId);
                    serviceOutPath.setParam2(patid);
                    serviceOutPath.setParam3(ivrTask1.getTaskName());
                    serviceOutPath.setParam6(subId);
                    serviceOutPath.setCreateTime(new Date());
                    serviceOutPath.setOrgid(serviceSubtask.getOrgid());
                    iServiceOutPathService.insertServiceOutPath(serviceOutPath);
                    String url = "";
                    String format = String.format("%03X", serviceOutPath.getId());
                    //先判断一下发的是不是宣教
                    if (!"3".equals(serviceSubtask.getType())) {
                        sendMagParam.setPhone(serviceSubtask.getPhone());
                        url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/wt?p=" + format : localIP + "/wt?p=" + format;
                        sendMagParam.setUrl(url);
                        //如果type是语音随访的话(说明补偿发送方式中有电话随访的方式,这里的外链就地址只能用/sf)
                        if (serviceSubtask.getType().equals("1"))
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/sf?p=" + format : localIP + "/sf?p=" + format;
                            sendMagParam.setUrl(url);
 
//                        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        SysConfig sysConfig = new SysConfig();
                        if (StringUtils.isNotEmpty(serviceSubtask.getServiceType()) && serviceSubtask.getServiceType().equals("6")) {
                            sysConfig = sysConfigMapper.checkConfigKeyUnique("zymyd.sms.template");
                        } else if (StringUtils.isNotEmpty(serviceSubtask.getServiceType()) && serviceSubtask.getServiceType().equals("14")) {
                            sysConfig = sysConfigMapper.checkConfigKeyUnique("mzmyd.sms.template");
                        } else {
                            sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        }
                        if (!Objects.isNull(sysConfig)) {
                            String smsContemt = sysConfig.getConfigValue();
//                            sendMagParam.setContent("您好,邀请您填写出院调查表,请点击" + sendMagParam.getUrl() + "填写。感谢您配合!");
                            sendMagParam.setContent(smsContemt.replace("${wjurl}", sendMagParam.getUrl()));
                        }
                    } else {
                        // ⚠️ 这里**刻意不**为 TemplateModuleDisabledException 做降级:
                        // 模板模块关闭(sys_config: template.enabled=false)时,宣教内容确实取不到,
                        // 短信也就确实发不出去。外层 try-catch 会如实记为"短信发送失败",
                        // 并通过 setSuccessPreachForm(..., "5") 按既有机制升级到下一个通道(公众号/人工)。
                        // 若在这里硬记成"发送成功",等于用一条假记录掩盖"没发出去",
                        // 事后从任务记录上完全看不出问题 —— 那是更坏的降级。
                        HeLibrary heLibrary = templateFacade.findHeLibraryById(serviceSubtask.getLibtemplateid());
                        if (heLibrary.getHetype().equals("1")) {
                            sendMagParam.setPhone(serviceSubtask.getPhone());
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/xj?p=" + format : localIP + "/xj?p=" + format;
 
                            sendMagParam.setUrl(url);
 
                            SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("xj.sms.template");
                            if (!Objects.isNull(sysConfig)) {
                                String smsContemt = sysConfig.getConfigValue();
//                              sendMagParam.setContent("您好,邀请您填写出院宣教调查表,请点击" + sendMagParam.getUrl() + "查看。感谢您配合!");
                                sendMagParam.setContent(smsContemt.replace("${xjurl}", sendMagParam.getUrl()));
 
                            }
                        } else if (heLibrary.getHetype().equals("2")) {
                            //通知
                            sendMagParam.setPhone(serviceSubtask.getPhone());
                            // 替换文本内容中的变量
                            String content = getObject(serviceSubtask, heLibrary.getPreachcontent());
                            sendMagParam.setContent(content);
                        }
 
                    }
 
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    //短信发送:各站点对接方式差异较大(报文/签名/token/返回判定都不同),
                    //已收口到 ruoyi-message 模块,这里只负责组装入参并路由
                    log.info("active的值为:{}", active);
                    SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam);
                    String isSuccess = smsSendResult.asLegacyFlag();
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
                    serviceSubtaskRecord.setSubtaskId(serviceSubtask.getId());
                    serviceSubtaskRecord.setUuid(UUID.randomUUID().toString());
                    serviceSubtaskRecord.setTasktype(serviceSubtask.getType());
                    serviceSubtaskRecord.setPreachform("4");
                    serviceSubtaskRecord.setStartTime(System.currentTimeMillis());
                    serviceSubtaskRecord.setResult("success");
                    if (StringUtils.isNotEmpty(isSuccess) && !isSuccess.equals("true"))
                        serviceSubtaskRecord.setResult("error");
                    serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
 
                    //发送完成后,修改状态
                    serviceSubtask.setSendstate(3L);
                    if (StringUtils.isNotEmpty(isSuccess) && isSuccess.equals("true")) {
                        serviceSubtask.setResult("success");
                        serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ",短信发送成功" : "短信发送成功");
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送成功", "2");
                    } else {
                        serviceSubtask.setResult("error");
                        serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ",短信发送失败" + now : "短信发送失败" + now);
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送失败" + now, "5");
                    }
                } catch (Exception e) {
                    String guid = UUID.randomUUID().toString();
                    log.error("【sfHandlle】长期任务短信发送失败,原因:{},GUID:{}, 时间: {}", e.getMessage(), guid, now);
                    setSuccessPreachForm(serviceSubtask, sendPreachform, "长期任务短信发送失败:系统错误, 时间: " + now, "5");
                }
            }
 
        } else if (descByCode.equals("公众号")) {
            log.info("【sfHandlle】处理公众号任务,任务信息:{}", ivrTask1);
            //公众号
            RSAPublicKeyExample rsaPublicKeyExample = new RSAPublicKeyExample();
            String taskId = rsaPublicKeyExample.encryptedData(ivrTask1.getTaskid().toString(), pub_key);
            boolean dateToday = isDateToday(serviceSubtask.getVisitTime());
            if (dateToday) {
                try {
                    String patid = rsaPublicKeyExample.encryptedData(serviceSubtask.getPatid().toString(), pub_key);
                    String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key);
                    String url = null;
                    url = localIP + ":" + req_path + "/outsideChainwt?param1=" + taskId + "&param2=" + patid + "&param3=" + URLEncoder.encode(ivrTask1.getTaskName(), StandardCharsets.UTF_8.toString()) + "&param5=false" + "$param6=" + subId;
 
                    ServiceOutPath serviceOutPath = new ServiceOutPath();
                    serviceOutPath.setParam1(taskId);
                    serviceOutPath.setParam2(patid);
                    serviceOutPath.setParam3(ivrTask1.getTaskName());
                    serviceOutPath.setParam6(subId);
                    serviceOutPath.setCreateTime(new Date());
                    serviceOutPath.setOrgid(serviceSubtask.getOrgid());
                    iServiceOutPathService.insertServiceOutPath(serviceOutPath);
                    String format = String.format("%03X", serviceOutPath.getId());
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
 
                    //微信公众号发送:市一与新华是两套完全不同的网关与判定标准,
                    //机构配置校验、HIS患者ID校验都已收口到 ruoyi-message 的发送方实现里
                    SendResult wechatSendResult = sendWechatByActive(serviceSubtask, patArchive, url);
                    Boolean wxCodeSuccess = wechatSendResult.isOk();
 
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
                    serviceSubtaskRecord.setUuid(UUID.randomUUID().toString());
                    serviceSubtaskRecord.setTasktype(serviceSubtask.getType());
                    serviceSubtaskRecord.setPreachform("4");
                    serviceSubtaskRecord.setStartTime(System.currentTimeMillis());
                    serviceSubtaskRecord.setResult("success");
                    if (!wxCodeSuccess) serviceSubtaskRecord.setResult("error");
                    serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
 
                    //发送完成后,修改状态
                    serviceSubtask.setSendstate(3L);
                    if (wxCodeSuccess) {
                        serviceSubtask.setResult("success");
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送成功", "2");
                    } else {
                        log.error("【sfHandlle】公众号发送失败,患者信息:{},错误信息:{}", serviceSubtask, wechatSendResult.getMessage());
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送失败", "5");
                    }
                } catch (Exception e) {
                    String guid = UUID.randomUUID().toString();
                    log.error("【sfHandlle】长期任务公众号发送失败,原因:{},GUID:{}", e.getMessage(), guid);
                    setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】长期任务公众号发送失败,原因:" + e.getMessage(), "5");
                }
            }
        } else if (descByCode.equals("人工")) {
            setSuccessPreachForm(serviceSubtask, sendPreachform, "人工发送成功", "2");
        }
 
    }
 
    /**
     * 按 {@code active}(站点)路由到对应的短信发送方。
     * <p>
     * 各站点差异见 {@code ruoyi-message} 模块各实现类;本方法只做「入参组装 + 路由 + 日志」。
     * <p>
     * ⚠️ 新华({@code xh})站点仍走「旧实现(发送空报文)」,与原 {@code RyTask} 行为一字不差,
     * 确认网关报文格式后应改为 {@code XinHuaSmsSender#sendForm}。详见该类的 TODO 说明。
     */
    private SendResult sendSmsByActive(ServiceSubtask serviceSubtask, SendMagParam sendMagParam) {
        SmsSendRequest request = SmsSendRequest.of(sendMagParam.getPhone(), sendMagParam.getContent(), serviceSubtask.getOrgid());
        //市一区分湖滨/吴山两套短信签名
        if ("hzszlyy".equals(active)) {
            request.setCampus(resolveShiyiCampus(serviceSubtask));
        }
        if ("xh".equals(active)) {
            XinHuaSmsSender xinHuaSmsSender = (XinHuaSmsSender) smsSenderFactory.get(active);
            return xinHuaSmsSender.sendJsonLegacy(request);
        }
        return smsSenderFactory.send(active, request);
    }
 
    /**
     * 按 {@code active}(站点)路由到对应的微信公众号发送方。
     * <p>
     * ⚠️ 新华公众号必须传患者信息(姓名 + HIS 患者ID),与原
     * {@code getWXCode(...)} 的入参保持一一对应。
     */
    private SendResult sendWechatByActive(ServiceSubtask serviceSubtask, PatArchive patArchive, String url) {
        WechatSendRequest request = new WechatSendRequest();
        request.setOrgid(patArchive.getOrgid());
        request.setIdCard(serviceSubtask.getSfzh());
        request.setUrl(url);
        request.setTitle(serviceSubtask.getTaskName());
        request.setContent(serviceSubtask.getTaskDesc());
        request.setPhone(patArchive.getTelcode());
        request.setPatientName(serviceSubtask.getSendname());
        request.setPatidHis(patArchive.getPatidHis());
        return wechatSenderFactory.send(active, request);
    }
 
    /**
     * 查询患者所在院区(1=湖滨,其他=吴山)。
     * <p>
     * TODO 待确认:原实现并未把 {@code sysDeptService.selectDeptList(...)} 的返回值赋给
     * {@code sysDepts},因此 {@code sysDepts} 恒为空列表、{@code campusid} 恒为 {@code "2"},
     * 湖滨院区实际上永远走不到 {@code sendSms1}。本次重构<b>保持该行为不变</b>,
     * 请在业务确认后再决定是否修成 {@code sysDepts = sysDeptService.selectDeptList(...)}。
     */
    private String resolveShiyiCampus(ServiceSubtask serviceSubtask) {
        SysDept sysDeptVo = new SysDept();
        List<SysDept> sysDepts = new ArrayList<>();
        if (ObjectUtils.isNotEmpty(serviceSubtask.getDeptcode())) {
            sysDeptVo.setDeptType("1");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(serviceSubtask.getDeptcode());
            sysDeptService.selectDeptList(sysDeptVo);
        } else if (ObjectUtils.isNotEmpty(serviceSubtask.getLeavehospitaldistrictcode())) {
            sysDeptVo.setDeptType("2");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(serviceSubtask.getLeavehospitaldistrictcode());
            sysDeptService.selectDeptList(sysDeptVo);
        }
        SysDept sysDept = CollectionUtils.isNotEmpty(sysDepts) ? sysDepts.get(0) : new SysDept();
        return ObjectUtils.isNotEmpty(sysDept) ? sysDept.getCampusid() : "2";
    }
 
    private Date getNextVisitTime(Long subid, Long taskid, Date visitTime) {
        ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
        serviceSubtaskPreachform.setSubid(subid);
        serviceSubtaskPreachform.setTaskid(taskid);
        List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
        if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) {
            String compensateTime = serviceSubtaskPreachforms.get(0).getCompensateTime();
            int hoursToAdd = Integer.valueOf(compensateTime) * 24;
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(visitTime);
            calendar.add(Calendar.HOUR_OF_DAY, hoursToAdd);
            Date newDate = calendar.getTime();
            return newDate;
        }
        return visitTime;
    }
 
    private String getSendPreachform(ServiceSubtaskPreachform serviceSubtaskPreachform, ServiceSubtask serviceSubtask) {
        List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
 
        if (CollectionUtils.isEmpty(serviceSubtaskPreachforms)) {
            //如果serviceSubtaskPreachforms为空的话,则需要进行新增一下
            List<ServiceSubtaskPreachform> serviceSubtaskPreachformList = addServiceSubtaskPreachform(serviceSubtask, "1");
            if (CollectionUtils.isEmpty(serviceSubtaskPreachformList)) {
                log.error("ryTask--发送方式失败了serviceSubtask的入参为:{}", serviceSubtask);
                return null;
            }
            serviceSubtaskPreachforms = serviceSubtaskPreachformList;
        }
 
        // 情况1:currentPreachform为空,取sort为1的preachform
        if (StringUtils.isEmpty(serviceSubtask.getCurrentPreachform())) {
            String firstPreachform = serviceSubtaskPreachforms.stream().filter(item -> item.getSort() == 1L).map(ServiceSubtaskPreachform::getPreachform).findFirst().orElse(null);
 
            //更新一下当前发送时间
            if (StringUtils.isNotEmpty(firstPreachform)) {
                serviceSubtaskPreachform.setPreachform(firstPreachform);
                serviceSubtaskPreachform.setCurrentSendTime(new Date());
                serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform);
            }
            return firstPreachform;
        }
 
        // 情况2:currentPreachform不为空,找下一个sort
        Optional<Long> currentSort = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getSort).findFirst();
 
        if (currentSort.isPresent()) {
            // 检查是否所有sendstate不等于9
            boolean allSendStateNot1AndNot9 = serviceSubtaskPreachforms.stream().allMatch(item -> !item.getSendstate().equals("9"));
            if (!allSendStateNot1AndNot9) {
                //说明,在某种方式发送失败后,又成功了(例如短信或公众号,在规定时间外患者做完了,这个时候,状态改成了9)
                serviceSubtask.setSendstate(6L);
                serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                return "-1";
            }
            // 当sort等于列表size时,并且所有sendstate都不等于9,则返回-1
            if (currentSort.get().equals((long) serviceSubtaskPreachforms.size())
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (allSendStateNot1AndNot9) {
                    //说明全发送失败了
                    serviceSubtask.setSendstate(7L);
                    serviceSubtask.setFinishtime(null);
                    log.error("getSendPreachform方法 检查是否所有sendstate不等于9, 全失败了");
                    serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有执行方式均未获取结果,任务失败(超时)" : "所有执行方式均未获取结果,任务失败(超时)");
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                    //修改发送状态
                    setFail(serviceSubtaskPreachforms, serviceSubtask);
                }
                return "-1";
            }
 
            // 当sort等于列表size时,下一个sort为-1
            Long nextSort = currentSort.get() + 1;
            //如果当前的发送方式不为空,并且开始准备要用下一种方式发送了,那当前的肯定没有发送成功,需要设置失败
            if (StringUtils.isNotEmpty(serviceSubtask.getCurrentPreachform())) {
                setFail(serviceSubtaskPreachforms, serviceSubtask);
            }
 
            // 找到下一个sort对应的preachform
            String nextPreachform = serviceSubtaskPreachforms.stream().filter(item -> nextSort.equals(item.getSort())).map(ServiceSubtaskPreachform::getPreachform).findFirst().orElse("-1");
 
            //更新一下当前发送时间
            if (StringUtils.isNotEmpty(nextPreachform)) {
                serviceSubtaskPreachform.setCurrentSendTime(new Date());
                serviceSubtaskPreachform.setPreachform(nextPreachform);
                serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform);
            }
            return nextPreachform;
        }
        return null;
    }
 
    private List<ServiceSubtaskPreachform> addServiceSubtaskPreachform(ServiceSubtask serviceSubtask, String sendstate) {
        //将preachformDesc的json对象,放到list集合中
        ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        String preachformDesc = serviceTask.getPreachformDesc();
 
        List<ServiceSubtaskPreachform> sspl = null;
        ObjectMapper objectMapper = new ObjectMapper();
        if (org.apache.commons.lang3.StringUtils.isNotEmpty(preachformDesc)) {
            try {
                sspl = objectMapper.readValue(preachformDesc, new TypeReference<List<ServiceSubtaskPreachform>>() {
                });
            } catch (JsonProcessingException e) {
                log.error("ryTask--preachform转List<ServiceSubtaskPreachform>报错了:{}", e.getMessage());
            }
            for (ServiceSubtaskPreachform serviceSubtaskPreachform : sspl) {
                serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
                serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
                //这个sendstate需要在数据库设置一个默认值为“1”
                serviceSubtaskPreachform.setSendstate("1");
                if (StringUtils.isNotEmpty(sendstate)) serviceSubtaskPreachform.setSendstate(sendstate);
                serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid());
                serviceSubtaskPreachformMapper.insertServiceSubtaskPreachform(serviceSubtaskPreachform);
            }
 
        }
 
        return sspl;
    }
 
    private Integer setFail(List<ServiceSubtaskPreachform> serviceSubtaskPreachforms, ServiceSubtask serviceSubtask) {
        Optional<Long> id = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getId).findFirst();
        if (id.isPresent()) {
            ServiceSubtaskPreachform subtaskPreachform = new ServiceSubtaskPreachform();
            subtaskPreachform.setSendstate("5");
            subtaskPreachform.setId(id.get());
            subtaskPreachform.setRemark("当前发送方式在规定时间内未成功获取结果");
            return serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(subtaskPreachform);
        }
        return 0;
    }
 
    // ---------------------------------------------------------------------
    // 说明:原 getDXCode / getWXCode / encode 三个私有方法已随发送方一起收口到
    // ruoyi-message 模块(LishuiSmsSender / XinHuaMpApi),此处已删除,避免重复实现。
    // ---------------------------------------------------------------------
 
    public void collectHIS() {
        try {
            // HIS数据采集
            log.info("【dealHisData】HIS开始采集数据");
            ichService.sync();
            log.info("【dealHisData】HIS结束采集数据");
        } catch (Exception e) {
            log.error("【dealHisData】HIS数据采集异常", e);
        }
    }
 
    public void collectHISYesterday() {
        try {
            // 获取昨天0点到今天0点的时间范围
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime yesterdayZero = todayZero.minusDays(1);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String startTime = yesterdayZero.format(formatter);
            String endTime = todayZero.format(formatter);
            // HIS数据采集
            log.info("【collectHISYesterday】HIS开始采集数据");
            // 立即返回,后台异步执行
            CompletableFuture.runAsync(() -> {
                ichService.syncMedInhosp(startTime, endTime);
            });
            log.info("【collectHISYesterday】HIS结束采集数据");
        } catch (Exception e) {
            log.error("【collectHISYesterday】HIS数据采集异常", e);
        }
    }
 
    public void collectHISDaysBefore(Long daysBefore) {
        try {
            // 获取daysBefore 0点到daysBefore-1 0点的时间范围
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime daysBeforeZeroStart = todayZero.minusDays(daysBefore);
            LocalDateTime daysBeforeZeroEnd = todayZero.minusDays(daysBefore-1L);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String startTime = daysBeforeZeroStart.format(formatter);
            String endTime = daysBeforeZeroEnd.format(formatter);
            // HIS数据采集
            log.info("【collectHISDaysBefore】HIS开始采集数据: startTime:{}, endtime:{}",startTime, endTime);
            // 立即返回,后台异步执行
            CompletableFuture.runAsync(() -> {
                ichService.syncMedInhosp(startTime, endTime);
            });
            log.info("【collectHISDaysBefore】HIS结束采集数据: startTime:{}, endtime:{}",startTime, endTime);
        } catch (Exception e) {
            log.error("【collectHISDaysBefore】HIS数据采集异常", e);
        }
    }
 
    public Boolean setSuccessPreachForm(ServiceSubtask serviceSubtask, String preachform, String remark, String failSendstate) {
        if (preachform.equals("-1")) {
            //说明都不需要执行了
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
            serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
            serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
            serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid());
            serviceSubtaskPreachform.setSendstate(failSendstate);
            serviceSubtaskPreachform.setCurrentSendTime(new Date());
            serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform);
            return true;
        }
 
        ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
        serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
        serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
        serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid());
        List<ServiceSubtaskPreachform> sspCount = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
 
        serviceSubtaskPreachform.setPreachform(preachform);
        //1:未发送状态
        serviceSubtaskPreachform.setSendstate("1");
        //电话的未发送状态,需要查2,因为,数据可能卡在队列里了; (没进队列,电话状态还是1)
        if (preachform.equals("3") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2)
            serviceSubtaskPreachform.setSendstate("2");
//        //人工的未发送状态,需要查2
//        if (preachform.equals("1") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2)
//            serviceSubtaskPreachform.setSendstate("2");
        log.error("-----serviceSubtaskPreachform的入参是?{}", serviceSubtaskPreachform);
        List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
        if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) {
            ServiceSubtaskPreachform serviceSubtaskPreachform1 = serviceSubtaskPreachforms.get(0);
            serviceSubtaskPreachform1.setSendstate(failSendstate);
            if (serviceSubtask.getType().equals("3") || serviceSubtask.getType().equals("4")) {
                //如果是宣教或通知,并且发送成功,直接将发送状态改成9
                if (failSendstate.equals("2")) serviceSubtaskPreachform1.setSendstate("9");
            }
            serviceSubtaskPreachform1.setRemark(remark);
            serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform1);
 
            //如果当前的preachform已经是最后一个了,那直接将serviceSubtask的sendstate状态改成7就行了(全超时了)
            if (serviceSubtaskPreachform1.getSort() == sspCount.size()
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (failSendstate.equals("4") || failSendstate.equals("5")) {
                    serviceSubtask.setCurrentPreachform(preachform);
                    serviceSubtask.setSendstate(7L);
                    log.error("setSuccessPreachForm方法,当前的preachform已经是最后一个了,全部执行失败");
                    serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有发送方式均未获取结果,任务失败(超时)" : "所有发送方式均未获取结果,任务失败(超时)");
                    //serviceSubtask.setRemark("setSuccessPreachForm方法,当前的preachform已经是最后一个了,全部执行失败");
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                    return true;
                }
            }
        } else {
            log.error("【定时任务中该患者没有查询到属于他的发送方式,subid:{},prechform:{},orgid:{}】", serviceSubtask.getId(), preachform, serviceSubtask.getOrgid());
            return false;
        }
 
        //将下一次的随访时间与当前执行方式记录一下
        serviceSubtask.setVisitTime(getNextVisitTime(serviceSubtask.getId(), serviceSubtask.getTaskid(), serviceSubtask.getVisitTime()));
        serviceSubtask.setCurrentPreachform(preachform);
        serviceSubtask.setSendstate(3L);
 
        if (serviceSubtask.getType().equals("3") || serviceSubtask.getType().equals("4")) {
            //如果是宣教或通知,并且发送成功,则直接将serviceSubtask状态改成6L(这个患者是没有题做的,不会再触发接口进行状态修改)
            if (failSendstate.equals("2")) {
                serviceSubtask.setSendstate(6L);
            }
        }
        serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + remark : remark);
        serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
        return true;
    }
 
    /**
     * 通配符变量替换
     *
     * @param serviceSubtask
     * @param scriptContent
     * @return
     */
    private String getObject(ServiceSubtask serviceSubtask, String scriptContent) {
        Map<String, Map<String, String>> param = getParam(serviceSubtask.getTaskid());
        for (Map<String, String> map : param.values()) {
            for (String key : map.keySet()) {
                scriptContent = scriptContent.replace(key, StringUtils.isNotEmpty(map.get(key)) ? map.get(key) : "");
            }
        }
        if (StringUtils.isEmpty(scriptContent)) {
            return scriptContent;
        }
        scriptContent = scriptContent.replace("${name}", StringUtils.isNotEmpty(serviceSubtask.getSendname()) ? serviceSubtask.getSendname() : "");
        scriptContent = scriptContent.replace("${dzz}", StringUtils.isNotEmpty(serviceSubtask.getAddr()) ? serviceSubtask.getAddr() : "");
        scriptContent = scriptContent.replace("${phone}", StringUtils.isNotEmpty(serviceSubtask.getTelcode()) ? serviceSubtask.getTelcode() : "");
 
        return scriptContent;
    }
 
    /**
     * 获取任务里的通配符
     *
     * @param taskId
     * @return
     */
    private Map<String, Map<String, String>> getParam(Long taskId) {
        ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(taskId);
        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Map<String, String>> serviceTaskMap = null;
        try {
            serviceTaskMap = objectMapper.readValue(serviceTask.getTextParam(), Map.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return serviceTaskMap;
    }
 
    public int qwenLLMTest(String questionText, String voiceText, String value, String regexText) {
        int result = QwenLLMUtil.matchRegex(questionText, voiceText, value, regexText);
        return result;
    }
 
    /**
     * 任务组(appltype=5)首轮纳入:扫描新出院患者,按 schedule 绑定的维度匹配任务组,
     * 为命中的患者按计划明细一次性生成第 0 轮 N 条 subtask,并置 schedulecheck_flag=1。
     * 后续轮次由 {@link #rollTaskGroupCycle()} 负责。
     * <p>
     * 与主扫描 dealOutHospInfo 分离,互不干扰。建议每 30 分钟执行一次。
     */
    public void dealTaskGroupOutHosp() {
        log.info("【dealTaskGroupOutHosp】开始扫描任务组新出院患者");
        if (!port.equals("8095")) {
            return;
        }
        try {
            String config = configService.selectConfigByKey("visit.early.day");
            // ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 dealOutHospInfo)。
            // 现走模块门面,与 RyTask#dealOutHospInfo 一致。
            inhospVisitFacade.dealOutHospInfoBySchedule(config);
            log.info("【dealTaskGroupOutHosp】任务组首轮纳入扫描完成");
        } catch (Exception e) {
            log.error("【dealTaskGroupOutHosp】任务组扫描异常", e);
        }
    }
 
    /**
     * 任务组(appltype=5)循环任务滚动调度。
     * <p>
     * 扫描所有 schedule_style=1 的计划,若本轮 subtask 全部 finalize(sendstate ∉ 1/2/3),
     * 则克隆本轮患者到下一轮,推进 current_loop_count 与 visit_time。
     * <p>
     * 建议每小时或每半小时执行一次,避免轮次积压。
     */
    public void rollTaskGroupCycle() {
        log.info("【rollTaskGroupCycle】开始扫描循环任务组");
        try {
            // ⚠️ 2025 修复(开关失效):原走 smartor 的 ServiceTaskScheduleServiceImpl.rollCycleTasks(),
            // 不判开关 —— 关掉 inhospvisit.enabled 照样「推进轮次 + 新建 subtask」。
            //
            // 现走模块门面,完成模块化方案里第 10 步一直没做的那一步切换:
            //   InhospVisitFacade#rollTaskGroupCycle(判开关)
            //     → DefaultInhospVisitApi#rollTaskGroupCycle
            //     → TaskGroupCycleRoller#rollCycleTasks(逐行搬自原 L170-190)
            //     → ScheduleRollPortBridge(smartor 侧端口实现,116 字段深拷贝等留在实现侧)
            // 该类的注释明确写了「第 10 步切过来之前不能被挂到任何调度上,否则双跑」——
            // 本行就是那个切换点,切完老的 rollCycleTasks 即退役(全仓只有本方法调它)。
            int rolled = inhospVisitFacade.rollTaskGroupCycle();
            // 门面内部的 TaskGroupCycleRoller 已打「本次推进 N 个计划」,这里只补一句总览
            log.info("【rollTaskGroupCycle】本次推进 {} 个计划", rolled);
        } catch (Exception e) {
            log.error("【rollTaskGroupCycle】滚动调度异常", e);
        }
    }
 
    /**
     * 随访任务是否超时
     * @param serviceSubtask
     * @param serviceSubtaskPreachforms
     * @return
     */
    public boolean isSubtaskDeadline(ServiceSubtask serviceSubtask, List<ServiceSubtaskPreachform> serviceSubtaskPreachforms){
        if (ObjectUtils.isNotEmpty(serviceSubtask)) {
            //只有出院随访和专病随访有超时
            if(StringUtils.isNotEmpty(serviceSubtask.getServiceType()) &&
                    !serviceSubtask.getServiceType().equals("2") && !serviceSubtask.getServiceType().equals("13")){
                return true;
            }
        }
        if (Objects.isNull(serviceSubtask) || CollectionUtils.isEmpty(serviceSubtaskPreachforms)) {
            return false;
        }
        // 基准时刻:出院时间 + 发送日(sendDay 缺省按 1),并兜底避免 NPE/脏数据
        ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        int sendDay = serviceTask != null && serviceTask.getSendDay() != null ? serviceTask.getSendDay().intValue() : 1;
        Date deadline = DateUtils.addDays(serviceSubtask.getEndtime(), sendDay);
        int sumCompensateTime = CollectionUtils.isEmpty(serviceSubtaskPreachforms) ? 0 : serviceSubtaskPreachforms.stream()
                .mapToInt(x -> x.getCompensateTime() == null ? 0 : Integer.parseInt(x.getCompensateTime()))
                .sum();
        sumCompensateTime = sumCompensateTime + 1;
        // 只补偿最后一步自身的补偿时间(天),脏值一律按 0 兜底
        deadline = DateUtils.addDays(deadline, sumCompensateTime);
        // 出院(发送)时间 + 本步补偿时间 小于当前时间 -> 超时
        return deadline.getTime() < new Date().getTime();
    }
 
    /**
     * 处理超时随访任务(随访超时 → 生成"再次随访")
     * <p>
     * ⚠️ 2025 修复(开关失效):本方法原不判任何模块开关 ——
     * 关掉 {@code inhospvisit.enabled} 照样建"再次随访"子任务。
     * <p>
     * <b>为什么这里是"入口判开关"而不是像 {@link #dealOutHospInfo()} 那样切到门面</b>:
     * 模块侧只提供了 {@code InhospVisitApi#buildAgainVisitDraft}(组装草稿),
     * <b>没有</b>完整的"扫描超时 → 落库 → 回写原任务状态"入口,
     * 而本方法自己组装并 insert(见下方 {@code insertServiceSubtask})。
     * 造一个模块入口属于新增能力、超出"只解决开关"的范围,所以取最小改动:入口守卫。
     * <p>
     * ⚠️ 全仓只有 {@code ServiceSubtaskController#dealSubtaskDeadline} 一个调用者。
     */
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay){
        // ⚠️ 开关守卫:先判再干活,与 dealOutHospInfo / rollTaskGroupCycle 同口径。
        // 关闭时直接返回,不扫描、不建"再次随访"子任务。
        if (!inhospVisitFacade.isEnabled()) {
            log.warn("【dealSubtaskDeadline】模块未启用(参数配置 inhospvisit.enabled=false),跳过超时随访处理。orgid={}", orgid);
            return;
        }
        //获取任务信息
        ServiceTask st = new ServiceTask();
        st.setDelFlag("0");
        st.setLongTask(1);
        st.setNexttaskflag("1");
        st.setOrgid(orgid);
        List<ServiceTask> serviceTasks = serviceTaskMapper.selectServiceTaskList(st);
        for (ServiceTask serviceTask : serviceTasks) {
            deadlineDay = -deadlineDay;
            Date deadline = DateUtils.addDays(new Date(), deadlineDay);
            //通过任务ID拿到患者信息,并且随访时间得是今天之前的
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setOrgid(orgid);
            serviceSubtaskVO.setSendstate(7L);
            //只扫描首次随访的
            serviceSubtaskVO.setVisitCount(1);
            serviceSubtaskVO.setTaskid(serviceTask.getTaskid());
            serviceSubtaskVO.setEndOutHospTime(deadline);
            List<ServiceSubtask> serviceSubtaskList
                    = 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());
                String config = configService.selectConfigByKey("visit.early.day");
                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());
                serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
                serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
                int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain);
                if (i == 1) {
                    //再次随访任务创建成功后,原任务改为失败状态
                    serviceSubtask.setSendstate(5L);
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                }
            }
        }
    }
 
}