puzhibing
2026-09-30 67d25f3d9eb0124c7aab544bf8f77031d6d8adc3
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
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
package com.ruoyi.order.service.impl;
 
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.feignClient.AppUserClient;
import com.ruoyi.account.api.feignClient.VinManagementClient;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.api.model.TVinManagement;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient;
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
import com.ruoyi.chargingPile.api.feignClient.ChargingPileClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
import com.ruoyi.chargingPile.api.model.TBillingTemplateDetail;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.bean.BeanUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
import com.ruoyi.integration.api.feignClient.SendMessageClient;
import com.ruoyi.integration.api.model.PlatformStopCharging;
import com.ruoyi.integration.api.model.TransactionRecord;
import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
import com.ruoyi.integration.api.vo.ChargingOrderVo;
import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
import com.ruoyi.order.api.model.TVinChargingOrder;
import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
import com.ruoyi.order.api.query.VinChargingOrderQuery;
import com.ruoyi.order.api.vo.ChargingOrderVO;
import com.ruoyi.order.api.vo.DeviceStartChargingVO;
import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
import com.ruoyi.order.api.vo.VinChargingStatusVO;
import com.ruoyi.order.dto.MyVinChargingOrderList;
import com.ruoyi.order.mapper.TVinChargingOrderMapper;
import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TVinChargingOrderService;
import com.ruoyi.order.support.ChargeAccountSupport;
import com.ruoyi.order.util.TransactionStageParser;
import com.ruoyi.order.util.mongodb.service.TransactionRecordService;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.other.api.domain.TFleet;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.api.dto.ChargeAuthorizationDTO;
import com.ruoyi.other.api.dto.ChargeTimeRange;
import com.ruoyi.other.api.dto.CompanyPreferentialDTO;
import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient;
import com.ruoyi.other.api.feignClient.CompanyPreferentialClient;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import org.redisson.api.RLock;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
 
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
 
@Service
public class TVinChargingOrderServiceImpl extends ServiceImpl<TVinChargingOrderMapper, TVinChargingOrder>
        implements TVinChargingOrderService {
 
    private static final Logger log = LoggerFactory.getLogger(TVinChargingOrderServiceImpl.class);
 
    /** VD 订单号前缀(设备VIN码充电) */
    private static final String ORDER_CODE_PREFIX_VD = "VD";
    /** 订单状态:启动中 */
    private static final int ORDER_STATUS_STARTING = 2;
    /** 订单状态:充电中 */
    private static final int ORDER_STATUS_CHARGING = 3;
    /** 订单状态:停止中 */
    private static final int ORDER_STATUS_STOPPING = 4;
    /** 订单状态:已结束 */
    private static final int ORDER_STATUS_FINISHED = 5;
    /** 订单状态:启动失败(沿用 t_charging_order 枚举约定:-1 为负值语义,前端按 status<0 渲染) */
    private static final int ORDER_STATUS_START_FAILED = -1;
    /** 充电枪状态:充电中 */
    private static final int GUN_STATUS_CHARGING = 4;
    /** 充电枪状态:空闲 */
    private static final int GUN_STATUS_IDLE = 2;
    /** VD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
    private static final String LOCK_VD_START_GUN = "VD_START_GUN:";
    /** VD 建单VIN锁前缀(VIN ID维度,防同VIN多枪并发启动,文档13.2) */
    private static final String LOCK_VD_START_VIN = "VD_START_VIN:";
    /** VD 结算锁前缀(订单号维度,防并发重复结算,文档13.3) */
    private static final String LOCK_VD_SETTLE = "VD_SETTLE:";
    /** VD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
    private static final int VD_STARTING_TIMEOUT_MINUTES = 30;
    /** VD 结算消费流水备注 */
    private static final String VD_SETTLE_CONSUME_REMARK = "VIN码充电结算";
    /** VD 结算重试队列(有序集合:member=订单号|VIN记录ID|重试次数,score=下次重试时间戳毫秒) */
    private static final String VD_SETTLE_RETRY_QUEUE = "VD_SETTLE_RETRY";
    /** VD 结算重试首次延迟(秒):结算失败后快速重试间隔 */
    private static final int VD_SETTLE_RETRY_FIRST_SECONDS = 60;
    /** VD 结算重试最大重试次数(超过后交由 10 分钟补偿任务兜底) */
    private static final int VD_SETTLE_RETRY_MAX_TIMES = 5;
    /** VD 结算重试退避上限(秒) */
    private static final int VD_SETTLE_RETRY_MAX_BACKOFF_SECONDS = 600;
    /** VD 结算重试单轮处理上限(防止单轮耗时过长) */
    private static final int VD_SETTLE_RETRY_BATCH_SIZE = 200;
    /** VD DB 兜底扫描单轮上限(现有10分钟补偿任务每轮最多处理的订单数) */
    private static final int VD_DB_SCAN_LIMIT = 200;
    /** 列表/详情回填 VIN 信息时,单次批量远程查询的最大 ids 数量 */
    private static final int VIN_BACKFILL_BATCH_SIZE = 500;
    /** 账户模式:共享(锁企业账户余额) */
    private static final int ACCOUNT_MODE_SHARED = 1;
    /** 账户模式:分配(锁VIN账户余额) */
    private static final int ACCOUNT_MODE_ALLOCATED = 2;
    /** 未配置充电授权时的默认下发金额上限(元) */
    private static final BigDecimal VD_DEFAULT_DISPATCH_AMOUNT = new BigDecimal("200");
    /** 授权时间到期停充提前量(秒):剩余授权时间不足该值时提前下发停充,规避数据传输延迟 */
    private static final int AUTHORIZE_STOP_AHEAD_SECONDS = 60;
    /** 授权时间方式:全天 */
    private static final int AUTHORIZE_TIME_MODE_ALL_DAY = 1;
    /** 授权时间方式:指定时段 */
    private static final int AUTHORIZE_TIME_MODE_RANGE = 2;
    /** 授权时间方式:指定电价时段 */
    private static final int AUTHORIZE_TIME_MODE_PRICE = 3;
    /** 企业优惠方式:不优惠 */
    private static final int PREFERENTIAL_TYPE_NONE = 1;
    /** 企业优惠方式:按比例优惠(服务费折扣) */
    private static final int PREFERENTIAL_TYPE_RATE = 2;
    /** 企业优惠方式:按优惠单价收费(取计费模板单价) */
    private static final int PREFERENTIAL_TYPE_TEMPLATE = 3;
    /** 金额比较用零值常量 */
    private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO;
 
    @Resource
    private TokenService tokenService;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private VinManagementClient vinManagementClient;
    @Resource
    private AppUserClient appUserClient;
    @Resource
    private SiteClient siteClient;
    @Resource
    private ChargingGunClient chargingGunClient;
    @Resource
    private ChargingPileClient chargingPileClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private UploadRealTimeMonitoringDataService monitoringDataService;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private AccountingStrategyDetailClient accountingStrategyDetailClient;
    @Resource
    private TVinAccountingStrategyOrderService vinAccountingStrategyOrderService;
    @Resource
    private TVinAccountingStrategyDetailOrderService vinAccountingStrategyDetailOrderService;
    @Resource
    private TVinChargingOrderAccountingStrategyService vinChargingOrderAccountingStrategyService;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private ChargeAuthorizationClient chargeAuthorizationClient;
    @Resource
    private CompanyPreferentialClient companyPreferentialClient;
    @Resource
    private BillingTemplateClient billingTemplateClient;
    @Resource
    private TransactionRecordService transactionRecordService;
    @Resource
    private ChargeAccountSupport chargeAccountSupport;
 
    @Override
    public PageInfo<ChargingOrderVO> pageList(VinChargingOrderQuery query) {
        int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize();
        setAuthSiteIds(query);
        PageInfo<ChargingOrderVO> page = new PageInfo<>(pageCurr, pageSize);
        long total = baseMapper.count(query);
        page.setTotal(total);
        List<ChargingOrderVO> records = total == 0 ? Collections.emptyList()
                : baseMapper.pageList(query, (long) (pageCurr - 1) * pageSize, pageSize);
        fillExtra(records);
        page.setRecords(records);
        return page;
    }
 
    @Override
    public PageInfo<MyVinChargingOrderList> pageAppletOrders(Integer pageCurr, Integer pageSize) {
        int current = pageCurr == null || pageCurr < 1 ? 1 : pageCurr;
        int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
        PageInfo<MyVinChargingOrderList> page = new PageInfo<>(current, size);
        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        Long appUserId = tokenService.getLoginUserApplet().getUserId();
        R<List<Long>> vinIdResponse = vinManagementClient.getVinIdsByAppUserId(appUserId);
        List<Long> vinIds = vinIdResponse == null ? Collections.emptyList()
                : Optional.ofNullable(vinIdResponse.getData()).orElse(Collections.emptyList());
        if (vinIds.isEmpty()) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        long total = baseMapper.countAppletOrders(vinIds);
        page.setTotal(total);
        List<MyVinChargingOrderList> records = total == 0 ? Collections.emptyList()
                : baseMapper.pageAppletOrders(vinIds, (long) (current - 1) * size, size);
        fillAppletExtra(records);
        page.setRecords(records);
        return page;
    }
 
    @Override
    public Map<String, Object> appletDetail(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedAppletOrder(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", String.valueOf(order.getId()));
        result.put("chargingGunId", order.getChargingGunId());
        result.put("code", order.getCode());
        result.put("createTime", order.getCreateTime() == null ? null
                : order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("status", order.getStatus());
        result.put("licensePlate", order.getPlateNum());
        result.put("startTime", toEpochMilli(order.getStartTime()));
        result.put("endTime", toEpochMilli(order.getEndTime()));
        result.put("orderAmount", order.getOrderAmount());
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("rechargeAmount", order.getRechargeAmount());
        result.put("couponDiscountAmount", order.getCouponDiscountAmount());
        result.put("vipDiscountAmount", order.getVipDiscountAmount());
        result.put("refundAmount", order.getRefundAmount());
        result.put("electricMoney", order.getElectrovalence());
        result.put("serviceMoney", order.getServiceCharge());
        result.put("totalElectricQuantity", order.getChargingCapacity());
        result.put("stageCost", baseMapper.accountingStrategyList(order.getId()));
        result.put("actionable", 0);
        result.put("isAppeal", 0);
 
        Site site = getSite(order.getSiteId());
        result.put("title", site == null ? "未知电站" : site.getName());
        TChargingGun gun = getGun(order.getChargingGunId());
        result.put("chargeMode", gun == null ? null : gun.getChargeMode());
        result.put("name", gun == null ? null : gun.getName());
        fillAppletMonitoring(result, order.getCode());
        return result;
    }
 
    @Override
    public List<VinChargingStatusVO> getChargingVinStatus(List<Long> vinIds) {
        if (vinIds == null || vinIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> distinctVinIds = vinIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        return distinctVinIds.isEmpty() ? Collections.emptyList() : baseMapper.getChargingVinStatus(distinctVinIds);
    }
 
    @Override
    public List<ChargingOrderVO> exportList(VinChargingOrderQuery query) {
        setAuthSiteIds(query);
        long total = baseMapper.count(query);
        List<ChargingOrderVO> result = new ArrayList<>();
        for (long offset = 0; offset < total; offset += 5000) {
            List<ChargingOrderVO> batch = baseMapper.pageList(query, offset, 5000);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        fillExtra(result);
        fillRealtimeData(result);
        return result;
    }
 
    @Override
    public Map<String, Object> summary(VinChargingOrderQuery query) {
        setAuthSiteIds(query);
        Map<String, Object> result = baseMapper.summary(query);
        return result == null ? new HashMap<>() : result;
    }
 
    @Override
    public Map<String, Object> detail(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", order.getId() == null ? null : String.valueOf(order.getId()));
        result.put("code", order.getCode());
        // 订单VIN快照为空时回填 VIN 管理登记值(历史订单无快照),前端按 actualVin → expectedVin → vin 取值
        TVinManagement vinRecord = order.getVinId() == null ? null
                : loadVinRecords(Collections.singletonList(order.getVinId())).get(order.getVinId());
        String vinCode = StringUtils.isEmpty(order.getActualVin()) ? order.getExpectedVin() : order.getActualVin();
        if (StringUtils.isEmpty(vinCode) && vinRecord != null) {
            vinCode = vinRecord.getVin();
        }
        result.put("vin", vinCode);
        result.put("actualVin", order.getActualVin());
        result.put("expectedVin", order.getExpectedVin());
        result.put("plateNum", StringUtils.isEmpty(order.getPlateNum()) && vinRecord != null
                ? vinRecord.getPlateNumber() : order.getPlateNum());
        result.put("phone", resolvePhone(order.getPhone(), order.getAppUserId(), vinRecord));
        result.put("status", order.getStatus());
        result.put("siteId", order.getSiteId());
        result.put("chargingPileId", order.getChargingPileId());
        result.put("chargingGunId", order.getChargingGunId());
        result.put("startTime", order.getStartTime());
        result.put("endTime", order.getEndTime());
        result.put("chargingCapacity", order.getChargingCapacity());
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("electrovalence", order.getElectrovalence());
        result.put("serviceCharge", order.getServiceCharge());
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<Site> sites = order.getSiteId() == null ? Collections.emptyList()
                : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(order.getSiteId())).getData()).orElse(Collections.emptyList());
        Site site = sites.stream().findFirst().orElse(null);
        result.put("siteName", site == null ? null : site.getName());
        List<TChargingGun> guns = order.getChargingGunId() == null ? Collections.emptyList()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(Collections.singletonList(order.getChargingGunId())).getData()).orElse(Collections.emptyList());
        TChargingGun gun = guns.stream().findFirst().orElse(null);
        result.put("terminalName", gun == null ? null : gun.getName());
        List<UploadRealTimeMonitoringData> monitoring = monitoringDataService.getDataByOrderCodeAsc(order.getCode());
        if (monitoring == null) {
            monitoring = Collections.emptyList();
        }
        for (UploadRealTimeMonitoringData data : monitoring) {
            if (data.getOutput_current() != null && data.getOutput_voltage() != null) {
                data.setPower(data.getOutput_voltage().multiply(data.getOutput_current()).divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP));
            }
        }
        UploadRealTimeMonitoringData latestMonitoring = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
        result.put("cdElectronic", latestMonitoring == null ? null : latestMonitoring.getOutput_current());
        result.put("cdVoltage", latestMonitoring == null ? null : latestMonitoring.getOutput_voltage());
        result.put("totalPower", latestMonitoring == null ? null : latestMonitoring.getPower());
        result.put("surplus", latestMonitoring == null ? null : latestMonitoring.getSoc());
        result.put("list", monitoring);
        result.put("list1", baseMapper.accountingStrategyList(order.getId()));
        return result;
    }
 
    @Override
    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
        Long orderId;
        try {
            orderId = Long.valueOf(chargingOrderId);
        } catch (NumberFormatException e) {
            return AjaxResult.error("VIN码充电订单ID无效");
        }
        TVinChargingOrder order = getAuthorizedOrder(orderId);
        if (order == null) {
            return AjaxResult.error("VIN码充电订单不存在或无权操作");
        }
        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
            return AjaxResult.error("充电枪与订单不匹配");
        }
        Integer status = order.getStatus();
        if (status == null || !status.equals(3)) {
            return AjaxResult.error(status != null && (status.equals(4) || status.equals(5)) ? "不能重复操作" : "还未开始充电");
        }
        TChargingGun chargingGun = chargingGunClient.getChargingGunById(chargingGunId).getData();
        TChargingPile chargingPile = order.getChargingPileId() == null ? null
                : chargingPileClient.getChargingPileById(order.getChargingPileId()).getData();
        if (chargingGun == null || chargingPile == null) {
            return AjaxResult.error("未找到订单对应的充电设备");
        }
        order.setStatus(4);
        order.setEndMode(1);
        order.setEndTime(LocalDateTime.now());
        if (!updateById(order)) {
            return AjaxResult.error("更新VIN码充电订单状态失败");
        }
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(chargingPile.getCode());
        command.setCharging_gun_code(chargingGun.getCode());
        sendMessageClient.platformStopCharging(command);
        return AjaxResult.success();
    }
 
    @Override
    public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
        return order == null ? Collections.emptyList() : baseMapper.accountingStrategyList(chargingOrderId);
    }
 
    private void setAuthSiteIds(VinChargingOrderQuery query) {
        List<Integer> authorizedSiteIds = getAuthorizedSiteIds();
        if (query.getSiteId() != null) {
            authorizedSiteIds = authorizedSiteIds.contains(query.getSiteId())
                    ? Collections.singletonList(query.getSiteId()) : Collections.singletonList(-1);
        }
        if (authorizedSiteIds.isEmpty()) {
            authorizedSiteIds = Collections.singletonList(-1);
        }
        query.setSiteIds(authorizedSiteIds);
    }
 
    private TVinChargingOrder getAuthorizedOrder(Long chargingOrderId) {
        if (chargingOrderId == null) {
            return null;
        }
        TVinChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) {
            return null;
        }
        return order;
    }
 
    private List<Integer> getAuthorizedSiteIds() {
        Long userId = tokenService.getLoginUser().getUserid();
        if (userId == null) {
            return Collections.emptyList();
        }
        return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData())
                .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
    }
 
    private TVinChargingOrder getAuthorizedAppletOrder(Long chargingOrderId) {
        if (chargingOrderId == null || tokenService.getLoginUserApplet() == null
                || tokenService.getLoginUserApplet().getUserId() == null) {
            return null;
        }
        TVinChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getVinId() == null) {
            return null;
        }
        R<List<Long>> response = vinManagementClient.getVinIdsByAppUserId(
                tokenService.getLoginUserApplet().getUserId());
        List<Long> vinIds = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return vinIds.contains(order.getVinId()) ? order : null;
    }
 
    private Long toEpochMilli(LocalDateTime time) {
        return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }
 
    private Site getSite(Integer siteId) {
        if (siteId == null) {
            return null;
        }
        R<List<Site>> response = siteClient.getSiteByIds(Collections.singletonList(siteId));
        List<Site> sites = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return sites.stream().findFirst().orElse(null);
    }
 
    private TChargingGun getGun(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        R<TChargingGun> response = chargingGunClient.getChargingGunById(gunId);
        return response == null ? null : response.getData();
    }
 
    private void fillAppletMonitoring(Map<String, Object> result, String orderCode) {
        List<UploadRealTimeMonitoringData> dataList = orderCode == null ? Collections.emptyList()
                : monitoringDataService.getDataByOrderCode(orderCode);
        if (dataList == null || dataList.isEmpty()) {
            result.put("currentAndVoltage", Collections.emptyList());
            result.put("power", Collections.emptyList());
            return;
        }
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
        Map<String, UploadRealTimeMonitoringData> minuteData = new java.util.LinkedHashMap<>();
        for (UploadRealTimeMonitoringData data : dataList) {
            if (data != null && data.getCreate_time() != null) {
                minuteData.putIfAbsent(formatter.format(data.getCreate_time()), data);
            }
        }
        List<Map<String, Object>> currentAndVoltage = new ArrayList<>();
        List<Map<String, Object>> power = new ArrayList<>();
        for (Map.Entry<String, UploadRealTimeMonitoringData> entry : minuteData.entrySet()) {
            UploadRealTimeMonitoringData data = entry.getValue();
            Map<String, Object> currentItem = new HashMap<>();
            currentItem.put("time", entry.getKey());
            currentItem.put("electricity", data.getOutput_current());
            currentItem.put("voltage", data.getOutput_voltage());
            currentAndVoltage.add(currentItem);
 
            Map<String, Object> powerItem = new HashMap<>();
            powerItem.put("time", entry.getKey());
            powerItem.put("soc", data.getSoc());
            BigDecimal outputPower = data.getOutput_current() == null || data.getOutput_voltage() == null ? null
                    : data.getOutput_current().multiply(data.getOutput_voltage())
                    .divide(new BigDecimal("1000"), 2, RoundingMode.HALF_DOWN);
            powerItem.put("power", outputPower);
            power.add(powerItem);
        }
        result.put("currentAndVoltage", currentAndVoltage);
        result.put("power", power);
    }
 
    private void fillAppletExtra(List<MyVinChargingOrderList> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> siteIds = list.stream().map(MyVinChargingOrderList::getSiteId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(MyVinChargingOrderList::getChargingGunId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Integer, Site> sites = siteIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        Map<Integer, TChargingGun> guns = gunIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(gunIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(TChargingGun::getId, item -> item, (a, b) -> a));
        for (MyVinChargingOrderList item : list) {
            Site site = sites.get(item.getSiteId());
            item.setTitle(site == null ? "未知电站" : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setName(gun == null ? null : gun.getName());
        }
    }
 
    private void fillExtra(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, Integer> periodCounts = new HashMap<>();
        if (!orderIds.isEmpty()) {
            for (Map<String, Object> item : baseMapper.countAccountingStrategies(orderIds)) {
                Number chargingOrderId = (Number) item.get("chargingOrderId");
                Number periodCount = (Number) item.get("periodCount");
                if (chargingOrderId != null && periodCount != null) {
                    periodCounts.put(chargingOrderId.longValue(), periodCount.intValue());
                }
            }
        }
        List<Integer> siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(ChargingOrderVO::getChargingGunId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Integer, Site> sites = siteIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        Map<Integer, TChargingGun> guns = gunIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(gunIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(TChargingGun::getId, item -> item, (a, b) -> a));
        for (ChargingOrderVO item : list) {
            Site site = sites.get(item.getSiteId());
            item.setSiteName(site == null ? null : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setTerminalName(gun == null ? null : gun.getName());
            if (item.getStartTime() != null && item.getEndTime() != null) {
                item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime()));
            }
            item.setCount(periodCounts.getOrDefault(item.getId(), 0));
            item.setPlatformIncome(BigDecimal.ZERO);
            item.setPartnerIncome(BigDecimal.ZERO);
        }
        fillVinInfoAndPhone(list);
        fillCompanyFleetNames(list);
    }
 
    /**
     * 回填 VIN 码/车牌号/手机号:订单快照为空时取 VIN 管理登记值(历史订单无快照),
     * 手机号再以 App 用户当前手机号兜底。远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillVinInfoAndPhone(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> vinIds = list.stream().map(ChargingOrderVO::getVinId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, TVinManagement> vinRecords = loadVinRecords(vinIds);
        for (ChargingOrderVO item : list) {
            TVinManagement vinRecord = item.getVinId() == null ? null : vinRecords.get(item.getVinId());
            if (vinRecord == null) {
                continue;
            }
            if (StringUtils.isEmpty(item.getVin())) {
                item.setVin(vinRecord.getVin());
            }
            if (StringUtils.isEmpty(item.getPlateNum())) {
                item.setPlateNum(vinRecord.getPlateNumber());
            }
            if (StringUtils.isEmpty(item.getPhone())) {
                item.setPhone(vinRecord.getPhone());
            }
        }
        List<Long> appUserIds = list.stream()
                .filter(item -> StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null)
                .map(ChargingOrderVO::getAppUserId).distinct().collect(Collectors.toList());
        if (appUserIds.isEmpty()) {
            return;
        }
        Map<Long, String> userPhones = loadAppUserPhones(appUserIds);
        for (ChargingOrderVO item : list) {
            if (StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null) {
                String phone = userPhones.get(item.getAppUserId());
                if (StringUtils.isNotEmpty(phone)) {
                    item.setPhone(phone);
                }
            }
        }
    }
 
    /**
     * 回填所属公司/车队名称:按订单快照 companyId/fleetId 批量远程取名(公司/车队改名后列表即时生效),
     * 远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillCompanyFleetNames(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> companyIds = list.stream().map(ChargingOrderVO::getCompanyId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (!companyIds.isEmpty()) {
            Map<Integer, String> companyNames = loadCompanyNames(companyIds);
            for (ChargingOrderVO item : list) {
                String name = item.getCompanyId() == null ? null : companyNames.get(item.getCompanyId());
                if (StringUtils.isNotEmpty(name)) {
                    item.setCompanyName(name);
                }
            }
        }
        List<Integer> fleetIds = list.stream().map(ChargingOrderVO::getFleetId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
        }
        Map<Integer, String> fleetNames = loadFleetNames(fleetIds);
        for (ChargingOrderVO item : list) {
            String name = item.getFleetId() == null ? null : fleetNames.get(item.getFleetId());
            if (StringUtils.isNotEmpty(name)) {
                item.setFleetName(name);
            }
        }
    }
 
    /** 批量查询公司名称,失败返回已取到的数据 */
    private Map<Integer, String> loadCompanyNames(List<Integer> companyIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TGroupCompany> companies = Optional.ofNullable(groupCompanyClient.listCompaniesByIds(companyIds).getData())
                    .orElse(Collections.emptyList());
            for (TGroupCompany company : companies) {
                if (company != null && company.getId() != null && StringUtils.isNotEmpty(company.getName())) {
                    result.put(company.getId(), company.getName());
                }
            }
        } catch (Exception e) {
            log.warn("[VD回填] 批量查询公司名称失败 ids={}", companyIds, e);
        }
        return result;
    }
 
    /** 批量查询车队名称,失败返回已取到的数据 */
    private Map<Integer, String> loadFleetNames(List<Integer> fleetIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TFleet> fleets = Optional.ofNullable(groupCompanyClient.listFleetsByIds(fleetIds).getData())
                    .orElse(Collections.emptyList());
            for (TFleet fleet : fleets) {
                if (fleet != null && fleet.getId() != null && StringUtils.isNotEmpty(fleet.getFleetName())) {
                    result.put(fleet.getId(), fleet.getFleetName());
                }
            }
        } catch (Exception e) {
            log.warn("[VD回填] 批量查询车队名称失败 ids={}", fleetIds, e);
        }
        return result;
    }
 
    /** 手机号回填优先级:订单值 → VIN 记录手机号 → App 用户当前手机号 */
    private String resolvePhone(String phone, Long appUserId, TVinManagement vinRecord) {
        if (StringUtils.isNotEmpty(phone)) {
            return phone;
        }
        if (vinRecord != null && StringUtils.isNotEmpty(vinRecord.getPhone())) {
            return vinRecord.getPhone();
        }
        if (appUserId == null) {
            return null;
        }
        return loadAppUserPhones(Collections.singletonList(appUserId)).get(appUserId);
    }
 
    /** 按 VIN 记录ID批量查询,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */
    private Map<Long, TVinManagement> loadVinRecords(List<Long> vinIds) {
        Map<Long, TVinManagement> result = new HashMap<>();
        if (vinIds == null || vinIds.isEmpty()) {
            return result;
        }
        for (int index = 0; index < vinIds.size(); index += VIN_BACKFILL_BATCH_SIZE) {
            List<Long> batch = vinIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, vinIds.size()));
            try {
                R<List<TVinManagement>> response = vinManagementClient.getVinByIds(new ArrayList<>(batch));
                if (response == null || response.getData() == null) {
                    continue;
                }
                for (TVinManagement record : response.getData()) {
                    if (record != null && record.getId() != null) {
                        result.putIfAbsent(record.getId(), record);
                    }
                }
            } catch (Exception e) {
                log.warn("[VD回填] 批量查询VIN记录失败 ids={}", batch, e);
            }
        }
        return result;
    }
 
    /** 按 App 用户ID批量查询当前手机号,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */
    private Map<Long, String> loadAppUserPhones(List<Long> appUserIds) {
        Map<Long, String> result = new HashMap<>();
        for (int index = 0; index < appUserIds.size(); index += VIN_BACKFILL_BATCH_SIZE) {
            List<Long> batch = appUserIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, appUserIds.size()));
            try {
                R<List<TAppUser>> response = appUserClient.getUserByIds(new ArrayList<>(batch));
                if (response == null || response.getData() == null) {
                    continue;
                }
                for (TAppUser user : response.getData()) {
                    if (user != null && user.getId() != null && StringUtils.isNotEmpty(user.getPhone())) {
                        result.putIfAbsent(user.getId(), user.getPhone());
                    }
                }
            } catch (Exception e) {
                log.warn("[VD回填] 批量查询App用户手机号失败 ids={}", batch, e);
            }
        }
        return result;
    }
 
    private void fillRealtimeData(List<ChargingOrderVO> list) {
        for (ChargingOrderVO item : list) {
            if (item.getCode() == null || item.getCode().trim().isEmpty()) {
                continue;
            }
            UploadRealTimeMonitoringData data = monitoringDataService.getLastDataById(item.getCode());
            if (data == null) {
                continue;
            }
            item.setChargeCurrent(data.getOutput_current());
            item.setChargeVoltage(data.getOutput_voltage());
            BigDecimal power = data.getPower();
            if (power == null && data.getOutput_current() != null && data.getOutput_voltage() != null) {
                power = data.getOutput_current().multiply(data.getOutput_voltage())
                        .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            }
            item.setChargePower(power);
        }
    }
 
    // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) =====
 
    @Override
    public DeviceStartChargingVO startVinCharging(DeviceStartChargingDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
            return DeviceStartChargingVO.fail(9);
        }
        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
        RLock gunLock = redissonClient.getLock(LOCK_VD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
        try {
            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
                return DeviceStartChargingVO.fail(10);
            }
            // 1. 充电枪校验
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(dto.getChargingPileCode());
            gunQuery.setCharging_gun_code(dto.getChargingGunCode());
            TChargingGun gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
            if (gun == null) {
                return DeviceStartChargingVO.fail(5);
            }
            if (gun.getStatus() != null && gun.getStatus() == GUN_STATUS_CHARGING) {
                return DeviceStartChargingVO.fail(10);
            }
            TChargingPile pile = gun.getChargingPileId() == null ? null
                    : chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData();
            Site site = gun.getSiteId() == null ? null
                    : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(gun.getSiteId())).getData())
                            .orElse(Collections.emptyList()).stream().findFirst().orElse(null);
 
            // 2. VIN反序解析与绑定校验(协议要求VIN码反序上送)
            String vin = new StringBuilder(dto.getAccount().trim()).reverse().toString();
            TVinManagement vinRecord = vinManagementClient.getVinByCode(vin).getData();
            if (vinRecord == null) {
                return DeviceStartChargingVO.fail(9);
            }
            if (vinRecord.getStatus() == null || vinRecord.getStatus() != 1) {
                return DeviceStartChargingVO.fail(2);
            }
 
            // 计费策略(枪策略优先,站点策略兜底):授权校验(指定电价时段)与策略快照共用
            Integer strategyId = gun.getAccountingStrategyId() != null
                    ? gun.getAccountingStrategyId()
                    : (site == null ? null : site.getAccountingStrategyId());
            // 3. 企业充电授权校验 + 账户额度计算(共享=企业账户,分配=VIN账户)
            VinChargeContext chargeContext = resolveVinChargeContext(gun.getSiteId(), strategyId, vinRecord);
            if (chargeContext.getFailCode() != null) {
                log.warn("[VD建单] 授权校验不通过 vinId={} gun={} reason={}", vinRecord.getId(),
                        dto.getChargingGunCode(), chargeContext.getFailReason());
                return DeviceStartChargingVO.fail(chargeContext.getFailCode());
            }
 
            // 并发防护(文档13.2):VIN ID维度锁,防同VIN多枪并发建单
            RLock vinLock = redissonClient.getLock(LOCK_VD_START_VIN + vinRecord.getId());
            boolean vinLocked = false;
            try {
                vinLocked = vinLock.tryLock(0, 30, TimeUnit.SECONDS);
                if (!vinLocked) {
                    return DeviceStartChargingVO.fail(10);
                }
                // 2.1 同VIN互斥:锁内按 vin_id 计数,存在未结账订单(启动中2/充电中3/停止中4)时拒绝新单
                if (existsUnsettledOrder(vinRecord.getId())) {
                    log.warn("[VD建单] 该VIN存在未结账订单,拒绝启动 vinId={} gun={}", vinRecord.getId(),
                            dto.getChargingGunCode());
                    return DeviceStartChargingVO.fail(10);
                }
                // 3. 建单(状态2 启动中)
                TVinChargingOrder order = new TVinChargingOrder();
                order.setCode(ORDER_CODE_PREFIX_VD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
                order.setOrderType(2);
                order.setOrderSource(1);
                order.setOrderClassification(2);
                // VD 无支付动作(后付记账模式,不执行在线支付/扣款):payMethod 留空,
                // 不套用 CD 余额支付枚举,避免下游按支付模式误判走余额扣款逻辑
                order.setStatus(ORDER_STATUS_STARTING);
                order.setTitle("【充电桩充电】" + (pile != null && pile.getNumber() != null ? pile.getNumber() : dto.getChargingPileCode())
                        + "号桩/" + dto.getChargingGunCode() + "号枪");
                order.setSiteId(gun.getSiteId());
                order.setChargingPileId(gun.getChargingPileId());
                order.setChargingGunId(gun.getId());
                order.setVinId(vinRecord.getId());
                order.setAppUserId(vinRecord.getAppUserId());
                // VIN 码/车牌快照落库(协议上报为反序VIN,此处存解析后的正序VIN),保证列表与详情直接可见
                order.setExpectedVin(vinRecord.getVin());
                order.setActualVin(vin);
                order.setPlateNum(vinRecord.getPlateNumber());
                // 所属公司/车队快照落库(VIN 管理记录当前归属),供订单列表/汇总/导出按公司、车队直接筛选
                order.setCompanyId(vinRecord.getCompanyId());
                order.setFleetId(vinRecord.getFleetId());
                order.setCreateTime(LocalDateTime.now());
                save(order);
 
                // 3.1 锁定账户额度(实扣冻结:可用余额减少、锁定金额增加;锁定失败则关单并回告余额不足)
                if (!lockVinChargeContext(vinRecord, chargeContext)) {
                    closeTimeoutStartingOrder(order);
                    return DeviceStartChargingVO.fail(3);
                }
                // 3.2 回填账户模式/锁定额/授权截止时间(订单列表与充电中授权到期停充使用)
                TVinChargingOrder accountUpdate = new TVinChargingOrder();
                accountUpdate.setId(order.getId());
                accountUpdate.setAccountMode(chargeContext.getAccountMode());
                accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
                accountUpdate.setMaxEndTime(chargeContext.getMaxEndTime());
                updateById(accountUpdate);
                // 同步内存对象,保证后续失败回滚能按账户模式/锁定额释放额度
                order.setAccountMode(chargeContext.getAccountMode());
                order.setLockedAmount(chargeContext.getDispatchAmount());
                order.setMaxEndTime(chargeContext.getMaxEndTime());
 
                // 4. 计费策略快照(严格模式:枪策略优先、站点策略兜底;策略缺失/查询不到/保存异常一律终止启动,
                //    避免无快照订单在结算时按 0 价计费):关单(状态-1)并释放锁定额度
                if (!saveStrategySnapshot(order.getId(), strategyId)) {
                    log.error("[VD建单] 计费策略快照保存失败,终止本次启动 code={} strategyId={}", order.getCode(), strategyId);
                    closeStartingOrderWithoutGunReset(order);
                    return DeviceStartChargingVO.fail(null);
                }
                log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={} accountMode={} lockAmount={} maxEndTime={}",
                        order.getCode(), vinRecord.getId(), dto.getChargingGunCode(), chargeContext.getAccountMode(),
                        chargeContext.getDispatchAmount(), chargeContext.getMaxEndTime());
                return DeviceStartChargingVO.success(order.getCode(), chargeContext.getDispatchAmount());
            } finally {
                if (vinLocked && vinLock.isHeldByCurrentThread()) {
                    vinLock.unlock();
                }
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[VD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
            return DeviceStartChargingVO.fail(null);
        } catch (Exception e) {
            log.error("[VD建单] VIN码充电启动异常 pile={} gun={} account={}",
                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
            return DeviceStartChargingVO.fail(null);
        } finally {
            if (gunLock.isHeldByCurrentThread()) {
                gunLock.unlock();
            }
        }
    }
 
    /**
     * VIN码充电账户上下文:账户模式、下发给充电桩的可用余额、授权截止时间与失败信息
     */
    private static class VinChargeContext {
        /** 账户模式(1=共享,2=分配) */
        private Integer accountMode;
        /** 锁定额度(元),同时作为下发给充电桩的可用余额 */
        private BigDecimal dispatchAmount;
        /** 授权截止时间(全天授权或未开启自动停充时为空) */
        private LocalDateTime maxEndTime;
        /** 失败码(为 null 表示校验通过) */
        private Integer failCode;
        /** 失败原因(仅日志排查用) */
        private String failReason;
 
        private static VinChargeContext fail(int failCode, String failReason) {
            VinChargeContext context = new VinChargeContext();
            context.failCode = failCode;
            context.failReason = failReason;
            return context;
        }
 
        private static VinChargeContext ok(Integer accountMode, BigDecimal dispatchAmount, LocalDateTime maxEndTime) {
            VinChargeContext context = new VinChargeContext();
            context.accountMode = accountMode;
            context.dispatchAmount = dispatchAmount;
            context.maxEndTime = maxEndTime;
            return context;
        }
 
        public Integer getAccountMode() {
            return accountMode;
        }
 
        public BigDecimal getDispatchAmount() {
            return dispatchAmount;
        }
 
        public LocalDateTime getMaxEndTime() {
            return maxEndTime;
        }
 
        public Integer getFailCode() {
            return failCode;
        }
 
        public String getFailReason() {
            return failReason;
        }
    }
 
    /**
     * 授权时间窗解析结果:maxEndTime 为空表示不做授权到期停充
     */
    private static class AuthorizationWindow {
        private final LocalDateTime maxEndTime;
 
        private AuthorizationWindow(LocalDateTime maxEndTime) {
            this.maxEndTime = maxEndTime;
        }
    }
 
    /**
     * 当前命中的电价阶段(授权时间方式=指定电价时段校验用)
     */
    private static class PeriodTypeWindow {
        private final Integer type;
        private final LocalTime endTime;
 
        private PeriodTypeWindow(Integer type, LocalTime endTime) {
            this.type = type;
            this.endTime = endTime;
        }
    }
 
    /**
     * 解析 VIN 码充电账户上下文(建单前调用):
     * 1) 校验公司充电授权(站点范围、可用时段/电价阶段),未配置授权按公司口径放行;
     * 2) 计算下发给充电桩的可用余额:共享模式=min(授权下发金额, 企业可用余额),
     *    分配模式=VIN 账户余额;账户可用余额为 0 时直接拒绝发起充电;
     * 3) 计算授权截止时间(充电中提前停充使用,未开启自动停充时为空)。
     */
    private VinChargeContext resolveVinChargeContext(Integer siteId, Integer strategyId, TVinManagement vinRecord) {
        Integer companyId = vinRecord.getCompanyId();
        if (companyId == null) {
            return VinChargeContext.fail(2, "VIN码未归属公司 vinId=" + vinRecord.getId());
        }
        TGroupCompany company;
        try {
            R<TGroupCompany> companyResult = groupCompanyClient.getCompanyById(companyId);
            company = companyResult == null ? null : companyResult.getData();
        } catch (Exception ex) {
            log.error("[VD建单] 查询所属公司失败 companyId={}", companyId, ex);
            return VinChargeContext.fail(2, "查询所属公司失败 companyId=" + companyId);
        }
        if (company == null) {
            return VinChargeContext.fail(2, "所属公司不存在 companyId=" + companyId);
        }
        if (company.getStatus() != null && company.getStatus() != 1) {
            return VinChargeContext.fail(2, "所属公司已停用 companyId=" + companyId);
        }
        int accountMode = company.getAccountMode() == null ? ACCOUNT_MODE_SHARED : company.getAccountMode();
 
        ChargeAuthorizationDTO authorization = null;
        try {
            R<ChargeAuthorizationDTO> authResult = chargeAuthorizationClient.getDetailByCompanyId(companyId);
            authorization = authResult == null ? null : authResult.getData();
        } catch (Exception ex) {
            log.error("[VD建单] 查询充电授权失败,按未配置授权放行 companyId={}", companyId, ex);
        }
        LocalDateTime maxEndTime = null;
        if (authorization != null) {
            if (authorization.getSiteIds() != null && !authorization.getSiteIds().isEmpty()
                    && (siteId == null || !authorization.getSiteIds().contains(siteId))) {
                return VinChargeContext.fail(5, "站点不在授权范围 siteId=" + siteId + " companyId=" + companyId);
            }
            AuthorizationWindow window = resolveAuthorizationWindow(authorization, strategyId, companyId);
            if (window == null) {
                return VinChargeContext.fail(2, "当前时间不在充电授权时段 companyId=" + companyId);
            }
            maxEndTime = window.maxEndTime;
        }
 
        BigDecimal usableAmount = accountMode == ACCOUNT_MODE_ALLOCATED ? vinRecord.getBalance()
                : company.getAvailableBalance();
        BigDecimal usable = usableAmount == null ? ZERO_AMOUNT : usableAmount;
        if (usable.compareTo(ZERO_AMOUNT) <= 0) {
            return VinChargeContext.fail(3, "账户可用余额为0 accountMode=" + accountMode + " companyId=" + companyId);
        }
        BigDecimal dispatch = authorization == null ? null : authorization.getDispatchAmount();
        if (dispatch == null || dispatch.compareTo(ZERO_AMOUNT) <= 0) {
            dispatch = VD_DEFAULT_DISPATCH_AMOUNT;
        }
        if (dispatch.compareTo(usable) > 0) {
            dispatch = usable;
        }
        return VinChargeContext.ok(accountMode, dispatch, maxEndTime);
    }
 
    /**
     * 锁定账户额度(共享=企业账户,分配=VIN账户),返回是否锁定成功
     */
    private boolean lockVinChargeContext(TVinManagement vinRecord, VinChargeContext context) {
        try {
            R<BigDecimal> result;
            if (Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(context.accountMode)) {
                result = vinManagementClient.lockBalance(vinRecord.getId(), context.dispatchAmount);
            } else {
                result = groupCompanyClient.lockBalance(vinRecord.getCompanyId(), context.dispatchAmount);
            }
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[VD建单] 锁定账户额度失败 accountMode={} companyId={} vinId={} amount={} msg={}",
                        context.accountMode, vinRecord.getCompanyId(), vinRecord.getId(), context.dispatchAmount,
                        result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[VD建单] 锁定账户额度异常 accountMode={} companyId={} vinId={} amount={}",
                    context.accountMode, vinRecord.getCompanyId(), vinRecord.getId(), context.dispatchAmount, ex);
            return false;
        }
    }
 
    /**
     * 释放账户锁定额度(订单未产生消费时使用:启动失败关单、异常终止等),失败仅记录日志由人工核对
     */
    private void unlockVinChargeContextQuietly(TVinChargingOrder order, String scene) {
        if (order == null || order.getLockedAmount() == null || order.getLockedAmount().compareTo(ZERO_AMOUNT) <= 0) {
            return;
        }
        Integer accountMode = order.getAccountMode();
        try {
            R<BigDecimal> result;
            if (Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(accountMode)) {
                result = vinManagementClient.unlockBalance(order.getVinId(), order.getLockedAmount());
            } else {
                result = groupCompanyClient.unlockBalance(order.getCompanyId(), order.getLockedAmount());
            }
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[VD账户] {}释放锁定额度失败 code={} accountMode={} amount={} msg={}", scene, order.getCode(),
                        accountMode, order.getLockedAmount(), result == null ? "null" : result.getMsg());
                return;
            }
            log.info("[VD账户] {}释放锁定额度成功 code={} accountMode={} amount={} balance={}", scene, order.getCode(),
                    accountMode, order.getLockedAmount(), result.getData());
        } catch (Exception ex) {
            log.error("[VD账户] {}释放锁定额度异常 code={} accountMode={} amount={}", scene, order.getCode(),
                    accountMode, order.getLockedAmount(), ex);
        }
    }
 
    /**
     * 校验充电授权时间方式并计算授权截止时间:
     * 1) 全天:不做时间校验,截止时间为空;
     * 2) 指定时段:当前时间须落在某段可用时段内,截止时间=该段结束时间(未开启自动停充时为空);
     * 3) 指定电价时段:当前电价阶段须在授权范围内,截止时间=该阶段结束时间(未开启自动停充时为空)。
     * 返回 null 表示当前时间不在授权范围内。
     */
    private AuthorizationWindow resolveAuthorizationWindow(ChargeAuthorizationDTO authorization, Integer strategyId,
                                                           Integer companyId) {
        ChargeAccountSupport.AuthorizationWindow window = chargeAccountSupport
                .resolveAuthorizationWindow(authorization, strategyId, companyId);
        return window == null ? null : new AuthorizationWindow(window.getMaxEndTime());
    }
 
    /**
     * 按计费策略明细匹配当前时间命中的电价阶段(HH:mm 口径,跨天/非法时段跳过;实现见公共支持类)
     */
    private PeriodTypeWindow findCurrentPeriodType(Integer strategyId, LocalTime current) {
        ChargeAccountSupport.PeriodTypeWindow period = chargeAccountSupport.findCurrentPeriodType(strategyId, current);
        return period == null ? null : new PeriodTypeWindow(period.getType(), period.getEndTime());
    }
 
    /**
     * 查询企业优惠设置(失败或未配置返回 null,按不优惠结算;实现见公共支持类)
     */
    private CompanyPreferentialDTO loadCompanyPreferential(Integer companyId) {
        return chargeAccountSupport.loadCompanyPreferential(companyId);
    }
 
    /**
     * 按优惠方式加载计费模板(仅方式3需要,取不到时该段按原价结算;实现见公共支持类)
     */
    private TBillingTemplateDTO loadBillingTemplate(CompanyPreferentialDTO preferential) {
        return chargeAccountSupport.loadBillingTemplate(preferential);
    }
 
    /**
     * 服务费按比例优惠:最终单价 = 原价 × (1 - 折扣率/100)(实现见公共支持类)
     */
    private static BigDecimal applyServiceDiscountRate(BigDecimal servicePrice, BigDecimal discountRate) {
        return ChargeAccountSupport.applyServiceDiscountRate(servicePrice, discountRate);
    }
 
    /**
     * 按电价阶段类型匹配计费模板明细(type:1=尖 2=峰 3=平 4=谷;实现见公共支持类)
     */
    private static TBillingTemplateDetail findTemplateDetail(TBillingTemplateDTO template, Integer type) {
        return ChargeAccountSupport.findTemplateDetail(template, type);
    }
 
    /**
     * 结算账户资金(实扣冻结口径):
     * 1) 解锁申请时锁定的额度,并按实际消费扣减(允许扣成负数),差额退回原账户;
     * 2) 消费流水由账户侧与余额变更一并写入(分配模式:VIN账户同事务;共享模式:企业账户),
     * 均以订单号幂等,结算失败回退订单状态后的补偿重试不会重复结算。
     * 旧订单(无锁定金额)保持既有"仅记账不扣款"行为。
     *
     * @return true=结算成功;false=结算失败(调用方需回退订单状态交由补偿任务重试)
     */
    private boolean settleVinChargeAccount(TVinChargingOrder order, BigDecimal actualAmount, String code) {
        BigDecimal lockedAmount = order.getLockedAmount() == null ? ZERO_AMOUNT : order.getLockedAmount();
        BigDecimal actual = actualAmount == null ? ZERO_AMOUNT : actualAmount;
        if (lockedAmount.compareTo(ZERO_AMOUNT) <= 0) {
            log.info("[VD结算] 订单无锁定金额,跳过账户结算 code={}", code);
            return true;
        }
        boolean allocated = Integer.valueOf(ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode());
        try {
            R<BigDecimal> settleResult = allocated
                    ? vinManagementClient.settleBalance(order.getVinId(), lockedAmount, actual, code,
                            order.getExpectedVin(), order.getPlateNum(), VD_SETTLE_CONSUME_REMARK)
                    : groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual, code,
                            null, null, VD_SETTLE_CONSUME_REMARK);
            if (settleResult == null || settleResult.getCode() != R.SUCCESS) {
                log.error("[VD结算] 账户结算失败 code={} accountMode={} locked={} actual={} msg={}", code,
                        order.getAccountMode(), lockedAmount, actual, settleResult == null ? "null" : settleResult.getMsg());
                return false;
            }
            log.info("[VD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code,
                    order.getAccountMode(), lockedAmount, actual, settleResult.getData());
            return true;
        } catch (Exception ex) {
            log.error("[VD结算] 账户结算异常 code={} accountMode={} locked={} actual={}", code, order.getAccountMode(),
                    lockedAmount, actual, ex);
            return false;
        }
    }
 
    /**
     * 保存VIN订单计费策略快照(主表 + 明细表)
     * <p>严格模式:策略未配置、远程查不到或保存异常均返回 false,由调用方关单终止本次启动,
     * 避免无快照订单在结算时按 0 价计费。</p>
     *
     * @return 是否成功落快照
     */
    private boolean saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (orderId == null) {
            return false;
        }
        if (strategyId == null) {
            log.error("[VD启动] 计费策略未配置,快照保存失败 orderId={}", orderId);
            return false;
        }
        try {
            R<TAccountingStrategy> strategyResult = accountingStrategyClient.getAccountingStrategyById(strategyId);
            TAccountingStrategy strategy = strategyResult == null ? null : strategyResult.getData();
            R<List<TAccountingStrategyDetail>> detailResult = accountingStrategyDetailClient
                    .getListByAccountingStrategyId(strategyId);
            List<TAccountingStrategyDetail> details = detailResult == null ? null : detailResult.getData();
            if (strategy == null || details == null || details.isEmpty()) {
                log.error("[VD启动] 计费策略或明细查询不到,快照保存失败 orderId={} strategyId={}", orderId, strategyId);
                return false;
            }
            TVinAccountingStrategyOrder strategyOrder = new TVinAccountingStrategyOrder();
            BeanUtils.copyProperties(strategy, strategyOrder);
            strategyOrder.setChargingOrderId(orderId);
            vinAccountingStrategyOrderService.save(strategyOrder);
            List<TVinAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
            for (TAccountingStrategyDetail detail : details) {
                TVinAccountingStrategyDetailOrder detailOrder = new TVinAccountingStrategyDetailOrder();
                BeanUtils.copyProperties(detail, detailOrder);
                detailOrder.setChargingOrderId(orderId);
                detailOrders.add(detailOrder);
            }
            vinAccountingStrategyDetailOrderService.saveBatch(detailOrders);
            return true;
        } catch (Exception ex) {
            log.error("[VD启动] 保存计费策略快照异常 orderId={} strategyId={}", orderId, strategyId, ex);
            return false;
        }
    }
 
    @Override
    public TVinChargingOrder getVinOrderByCode(String code) {
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getCode, code)
                .last("limit 1"));
    }
 
    @Override
    public void vinStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(vo.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只有启动中订单可消费启动应答,防止迟到消息复活已关闭/已结束订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) {
            return;
        }
        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
            boolean updated = lambdaUpdate()
                    .eq(TVinChargingOrder::getId, order.getId())
                    .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                    .set(TVinChargingOrder::getStartTime, LocalDateTime.now())
                    .update();
            if (!updated) {
                return;
            }
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功");
            log.info("[VD启动] VIN订单进入充电中 code={}", order.getCode());
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
        } else {
            closeTimeoutStartingOrder(order);
            log.warn("[VD启动] VIN订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
        }
    }
 
    @Override
    public boolean handleHandshakeVin(String orderCode, String vin) {
        if (StringUtils.isEmpty(orderCode)) {
            return false;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            log.warn("[VD握手] 订单不存在 code={}", orderCode);
            return false;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        boolean needUpdate = false;
        if (StringUtils.isNotEmpty(vin)) {
            update.setActualVin(vin);
            needUpdate = true;
        }
        boolean advanced = order.getStatus() != null && order.getStatus() == ORDER_STATUS_STARTING;
        if (advanced) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
            needUpdate = true;
        }
        if (!needUpdate) {
            return true;
        }
        boolean updated;
        if (advanced) {
            updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                    .eq(TVinChargingOrder::getId, order.getId())
                    .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
        } else {
            updated = updateById(update);
        }
        if (advanced && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "握手恢复");
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
            log.info("[VD握手] 握手推进订单进入充电中 code={}", orderCode);
        }
        return updated;
    }
 
    @Override
    public void vinChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(query.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只接收启动中/充电中的监控数据,禁止迟到数据复活终态订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())
                && !Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus())) {
            return;
        }
        // 充电中授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 下发,规避数据传输延迟)
        if (Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus()) && needAuthorizeStop(order)) {
            stopByAuthorizeExpired(order);
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        boolean starting = Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus());
        if (starting) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
        }
        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
                && query.getOutput_voltage() != null) {
            BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage())
                    .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            update.setChargingPower(power);
            update.setCurrent(query.getOutput_current());
            update.setVoltage(query.getOutput_voltage());
            update.setPower(power);
            update.setNeedElec(query.getOutput_current());
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setChargingPower(power);
            chargingGunClient.updateChargingGunById(gun);
        }
        if (query.getCharging_degree() != null) {
            update.setChargingCapacity(query.getCharging_degree());
            update.setElectricity(query.getCharging_degree());
        }
        boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getId, order.getId())
                .in(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING));
        if (starting && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复");
        }
    }
 
    /**
     * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟):
     * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电;
     * maxEndTime 为空(全天授权/未开启自动停充)时不介入。
     */
    private boolean needAuthorizeStop(TVinChargingOrder order) {
        LocalDateTime startTime = order.getStartTime() != null ? order.getStartTime() : order.getCreateTime();
        return chargeAccountSupport.needAuthorizeStop(order.getMaxEndTime(), startTime);
    }
 
    /**
     * 授权时间到期主动停充:复用小程序主动停充链路(状态4 + 结束方式1 + 下发停止充电命令),
     * 状态推进使用条件更新,与小程序停充/设备回调并发时只有一方生效
     */
    private void stopByAuthorizeExpired(TVinChargingOrder order) {
        TChargingGun gun;
        TChargingPile pile;
        try {
            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(order.getChargingGunId());
            gun = gunResult == null ? null : gunResult.getData();
            pile = order.getChargingPileId() == null ? null
                    : Optional.ofNullable(chargingPileClient.getChargingPileById(order.getChargingPileId()).getData())
                            .orElse(null);
        } catch (Exception ex) {
            log.error("[VD停充] 授权到期停充查询设备异常 code={}", order.getCode(), ex);
            return;
        }
        if (gun == null || pile == null) {
            log.error("[VD停充] 授权到期停充缺少设备信息 code={} gunId={}", order.getCode(), order.getChargingGunId());
            return;
        }
        boolean updated = lambdaUpdate()
                .eq(TVinChargingOrder::getId, order.getId())
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                .set(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .set(TVinChargingOrder::getEndMode, 1)
                .set(TVinChargingOrder::getEndTime, LocalDateTime.now())
                .update();
        if (!updated) {
            return;
        }
        order.setStatus(ORDER_STATUS_STOPPING);
        order.setEndMode(1);
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(pile.getCode());
        command.setCharging_gun_code(gun.getCode());
        sendMessageClient.platformStopCharging(command);
        log.warn("[VD停充] 授权时间到期主动停充 code={} maxEndTime={}", order.getCode(), order.getMaxEndTime());
    }
 
    @Override
    public void vinEndCharge(String orderCode, Integer endMode) {
        if (StringUtils.isEmpty(orderCode)) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(endMode);
        updateById(update);
        pushRegulatoryAsync(order, false, false);
    }
 
    @Override
    public R vinEndChargeBillingCharge(TransactionRecordMessageVO vo) {
        String code = vo == null ? null : vo.getTransaction_serial_number();
        log.info("[VD结算] 收到交易记录 code={}", code);
        if (StringUtils.isEmpty(code)) {
            return R.fail("交易记录缺少交易流水号");
        }
        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等第二层兜底
        RLock settleLock = redissonClient.getLock(LOCK_VD_SETTLE + code);
        try {
            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.warn("[VD结算] 并发结算进行中,跳过重复账单 code={}", code);
                return R.ok("重复账单,结算处理中");
            }
            return doVinEndChargeBillingCharge(vo, code);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return R.fail("结算锁获取被中断:" + code);
        } finally {
            if (settleLock.isHeldByCurrentThread()) {
                settleLock.unlock();
            }
        }
    }
 
    /**
     * VD 结算实际执行体(调用前须持有订单号结算锁)
     */
    private R doVinEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
        TVinChargingOrder order = getVinOrderByCode(code);
        if (order == null) {
            return R.fail("VIN码充电订单不存在:" + code);
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
            log.warn("[VD结算] 重复账单跳过 code={}", code);
            return R.ok("重复账单,已按首次账单结算");
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
            return R.ok("订单已关闭,跳过结算");
        }
 
        List<TransactionStageParser.PeriodElectricity> reportedPeriods =
                TransactionStageParser.parsePeriodElectricity(vo);
        BigDecimal totalElectricity = reportedPeriods.stream()
                .map(TransactionStageParser.PeriodElectricity::getElectricity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
            totalElectricity = vo.getTotal_electricity();
        }
 
        List<TVinAccountingStrategyDetailOrder> snapshots = new ArrayList<>(vinAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list());
        snapshots.removeIf(item -> item.getType() == null);
        snapshots.sort(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
                Comparator.nullsLast(String::compareTo)));
 
        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费)
        CompanyPreferentialDTO preferential = loadCompanyPreferential(order.getCompanyId());
        TBillingTemplateDTO billingTemplate = loadBillingTemplate(preferential);
 
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        BigDecimal discountElectricTotal = BigDecimal.ZERO;
        BigDecimal discountServiceTotal = BigDecimal.ZERO;
        List<TVinChargingOrderAccountingStrategy> details = new ArrayList<>();
        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
            TVinAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex);
            BigDecimal capacity = TransactionStageParser.getPeriodElectricity(
                    vo, snapshot.getType(), snapshotIndex + 1);
            if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
            BigDecimal originalServicePrice = snapshot.getCostServiceCharge() == null
                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
            // 原价金额(策略单价),优惠金额 = 原价 - 最终
            BigDecimal originalElectricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            // 优惠后的最终单价:方式2=服务费按比例优惠;方式3=电费/服务费取计费模板同阶段单价
            BigDecimal finalElectricPrice = electricPrice;
            BigDecimal finalServicePrice = servicePrice;
            if (preferential != null && Integer.valueOf(PREFERENTIAL_TYPE_RATE).equals(preferential.getPreferentialType())) {
                finalServicePrice = applyServiceDiscountRate(servicePrice, preferential.getServiceDiscountRate());
            } else if (preferential != null
                    && Integer.valueOf(PREFERENTIAL_TYPE_TEMPLATE).equals(preferential.getPreferentialType())) {
                TBillingTemplateDetail templateDetail = findTemplateDetail(billingTemplate, snapshot.getType());
                if (templateDetail == null) {
                    log.warn("[VD结算] 计费模板缺少对应阶段,该段按原价结算 code={} type={} templateId={}", code,
                            snapshot.getType(), preferential.getBillingTemplateId());
                } else {
                    if (templateDetail.getElectrovalence() != null) {
                        finalElectricPrice = templateDetail.getElectrovalence();
                    }
                    if (templateDetail.getServiceCharge() != null) {
                        finalServicePrice = templateDetail.getServiceCharge();
                    }
                }
            }
            BigDecimal electricAmount = capacity.multiply(finalElectricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal serviceAmount = capacity.multiply(finalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal discountElectricAmount = originalElectricAmount.subtract(electricAmount);
            BigDecimal discountServiceAmount = originalServiceAmount.subtract(serviceAmount);
            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
            discountElectricTotal = discountElectricTotal.add(discountElectricAmount);
            discountServiceTotal = discountServiceTotal.add(discountServiceAmount);
 
            TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy();
            detail.setChargingOrderId(order.getId());
            detail.setType(snapshot.getType());
            detail.setElectrovalence(finalElectricPrice);
            detail.setServiceCharge(finalServicePrice);
            detail.setCostServiceCharge(originalServicePrice);
            detail.setChargingCapacity(capacity);
            detail.setPeriodElectricPrice(electricAmount);
            detail.setPeriodServicePrice(serviceAmount);
            detail.setPeriodOriginalServicePrice(originalServiceAmount);
            detail.setPeriodOriginalElectricPrice(originalElectricAmount);
            detail.setPeriodDiscountElectricPrice(discountElectricAmount);
            detail.setPeriodDiscountServicePrice(discountServiceAmount);
            if (snapshot != null) {
                // 计费策略明细id:快照id(IdType.INPUT复制自原明细 id),与CD结算口径一致
                detail.setAccountingStrategyDetailId(snapshot.getId());
                // 时段位:与 CD 充电 TaskUtil.endOrder 同口径,截取快照时段的 HH:mm
                // (snapshot 已为 HH:mm;split 兜底兼容上游拼接的 yyyy-MM-dd HH:mm 情形)
                detail.setStartTime(splitPeriodHourMinute(snapshot.getStartTime()));
                detail.setEndTime(splitPeriodHourMinute(snapshot.getEndTime()));
            }
            // 首次添加时间:结算落库时刻(CD结算同口径)
            detail.setCreateTime(LocalDateTime.now());
            details.add(detail);
        }
        if (snapshots.isEmpty()) {
            log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
        LocalDateTime settlementStartTime = parseDeviceTime(vo.getStart_time(), order.getStartTime());
        LocalDateTime settlementEndTime = parseDeviceTime(vo.getEnd_time(), order.getEndTime());
        if (!details.isEmpty()) {
            DateTimeFormatter hhmm = DateTimeFormatter.ofPattern("HH:mm");
            if (settlementStartTime != null) {
                details.get(0).setStartTime(settlementStartTime.format(hhmm));
            }
            if (settlementEndTime != null) {
                details.get(details.size() - 1).setEndTime(settlementEndTime.format(hhmm));
            }
        }
        BigDecimal orderAmount = electrovalenceTotal.add(serviceChargeTotal);
        // lambda 引用要求有效final,拷贝最终值
        final BigDecimal totalElec = totalElectricity;
        final BigDecimal elecTotal = electrovalenceTotal;
        final BigDecimal svcTotal = serviceChargeTotal;
        final BigDecimal discountElecTotal = discountElectricTotal;
        final BigDecimal discountSvcTotal = discountServiceTotal;
        final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc");
        final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc");
 
        // 本地事务:写计费明细 + 更新订单(状态5;账户结算在事务外执行,避免长事务占用链接)
        transactionTemplate.execute(status -> {
            vinChargingOrderAccountingStrategyService.lambdaUpdate()
                    .eq(TVinChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
                    .remove();
            if (!details.isEmpty()) {
                vinChargingOrderAccountingStrategyService.saveBatch(details);
            }
            TVinChargingOrder update = new TVinChargingOrder();
            update.setId(order.getId());
            update.setTotalElectricity(totalElec);
            update.setChargingCapacity(totalElec);
            update.setElectrovalence(elecTotal);
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            update.setDiscountElectrovalence(discountElecTotal);
            update.setDiscountServiceCharge(discountSvcTotal);
            if (startSoc != null) {
                update.setStartSoc(startSoc);
            }
            if (endSoc != null) {
                update.setEndSoc(endSoc);
            }
            update.setEndTime(parseDeviceTime(vo.getEnd_time(), LocalDateTime.now()));
            if (order.getStartTime() == null) {
                update.setStartTime(parseDeviceTime(vo.getStart_time(), null));
            }
            update.setStatus(ORDER_STATUS_FINISHED);
            if (order.getEndMode() == null) {
                update.setEndMode(1);
            }
            updateById(update);
            return true;
        });
 
        // 账户结算:解锁申请时锁定的额度、按实际消费扣款并退回差额,消费流水由账户侧按订单号幂等写入;
        // 复位充电枪(设备侧充电已结束,复位与资金结算解耦,重试重复复位幂等)
        boolean settled = settleVinChargeAccount(order, orderAmount, code);
        resetGunQuietly(order);
        if (!settled) {
            // 结算失败:回退订单状态 5 -> 4(停止中),投递 1 分钟重试队列,10 分钟补偿任务对状态4/5做 DB 兜底;
            // 重试不会重复扣款(账户侧按订单号幂等)
            rollbackFinishedOrderForSettleRetry(order, code);
            enqueueVinSettleRetry(order);
            return R.fail("账户结算失败,订单状态已回退待重试:" + code);
        }
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[VD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={} discountElec={} discountSvc={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal,
                discountServiceTotal);
        return R.ok();
    }
 
    /**
     * 结算失败回退订单状态:仅当订单仍为已结束(5)时回退为停止中(4),
     * 由 1 分钟重试队列与 10 分钟补偿任务(listTimeoutChargingOrders 含状态4)重新结算
     */
    private void rollbackFinishedOrderForSettleRetry(TVinChargingOrder order, String code) {
        boolean rolledBack = lambdaUpdate()
                .eq(TVinChargingOrder::getId, order.getId())
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_FINISHED)
                .set(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .update();
        log.warn("[VD结算] 账户结算失败,回退订单状态 5->4 待重试 code={} rolledBack={}", code, rolledBack);
    }
 
    /**
     * 投递结算重试队列(有序集合:member=订单号|VIN记录ID|重试次数,score=下次重试时间戳毫秒),
     * 由 1 分钟任务 {@link #retryPendingVinSettle()} 主触发重试,失败指数退避、超次后交由 10 分钟补偿任务兜底
     */
    private void enqueueVinSettleRetry(TVinChargingOrder order) {
        if (order == null || StringUtils.isEmpty(order.getCode())) {
            return;
        }
        try {
            double nextRetryTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(VD_SETTLE_RETRY_FIRST_SECONDS);
            redissonClient.getScoredSortedSet(VD_SETTLE_RETRY_QUEUE)
                    .add(nextRetryTime, buildVinSettleRetryMember(order.getCode(), order.getVinId(), 1));
            log.info("[VD结算重试] 已投递重试队列 code={} vinId={} 首次延迟={}s", order.getCode(), order.getVinId(),
                    VD_SETTLE_RETRY_FIRST_SECONDS);
        } catch (Exception ex) {
            log.error("[VD结算重试] 投递重试队列异常,交由10分钟补偿任务兜底 code={}", order.getCode(), ex);
        }
    }
 
    private static String buildVinSettleRetryMember(String code, Long vinId, int times) {
        return code + "|" + (vinId == null ? "" : vinId) + "|" + times;
    }
 
    @Override
    public int retryPendingVinSettle() {
        RScoredSortedSet<String> queue = redissonClient.getScoredSortedSet(VD_SETTLE_RETRY_QUEUE);
        Collection<String> dueMembers;
        try {
            dueMembers = queue.valueRange(0, true, (double) System.currentTimeMillis(), true, 0,
                    VD_SETTLE_RETRY_BATCH_SIZE);
        } catch (Exception ex) {
            log.error("[VD结算重试] 读取重试队列异常", ex);
            return 0;
        }
        if (dueMembers == null || dueMembers.isEmpty()) {
            return 0;
        }
        int processed = 0;
        for (String member : dueMembers) {
            try {
                retryOneVinSettle(queue, member);
                processed++;
            } catch (Exception ex) {
                log.error("[VD结算重试] 处理队列成员异常 member={}", member, ex);
            }
        }
        log.info("[VD结算重试] 本轮到期重试处理数量={}", processed);
        return processed;
    }
 
    /**
     * 处理单条结算重试:成功/订单已终态/超次/记录缺失达到上限时出队,否则重入队列并指数退避
     */
    private void retryOneVinSettle(RScoredSortedSet<String> queue, String member) {
        String[] parts = member == null ? new String[0] : member.split("\\|");
        if (parts.length < 2 || StringUtils.isEmpty(parts[0])) {
            log.error("[VD结算重试] 队列成员格式非法,直接出队 member={}", member);
            queue.remove(member);
            return;
        }
        String code = parts[0];
        int times = parts.length >= 3 ? parseRetryTimes(parts[2]) : 0;
        TVinChargingOrder order = getVinOrderByCode(code);
        if (order == null) {
            log.warn("[VD结算重试] 订单不存在,出队 code={}", code);
            queue.remove(member);
            return;
        }
        Integer status = order.getStatus();
        if (status == null || status == ORDER_STATUS_FINISHED || status == ORDER_STATUS_START_FAILED) {
            log.info("[VD结算重试] 订单已终态,出队 code={} status={}", code, status);
            queue.remove(member);
            return;
        }
        TransactionRecord record = transactionRecordService.findOne(code);
        if (record == null || StringUtils.isEmpty(record.getResult())) {
            rescheduleVinSettleRetry(queue, member, order, times, "设备交易记录缺失");
            return;
        }
        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
        BeanUtils.copyProperties(record, vo);
        R result = vinEndChargeBillingCharge(vo);
        if (result != null && result.getCode() == R.SUCCESS) {
            log.info("[VD结算重试] 结算成功 code={} times={}", code, times);
            queue.remove(member);
            return;
        }
        rescheduleVinSettleRetry(queue, member, order, times, "结算仍未成功");
    }
 
    /**
     * 重入队列并指数退避:延迟 = min(首次延迟 × 2^(n-1), 退避上限),超过最大次数出队交由 10 分钟补偿任务兜底
     */
    private void rescheduleVinSettleRetry(RScoredSortedSet<String> queue, String member, TVinChargingOrder order,
                                          int times, String reason) {
        queue.remove(member);
        int nextTimes = times + 1;
        if (nextTimes > VD_SETTLE_RETRY_MAX_TIMES) {
            log.error("[VD结算重试] 重试次数耗尽,交由10分钟补偿任务兜底 code={} times={} reason={}", order.getCode(),
                    nextTimes, reason);
            return;
        }
        long delaySeconds = Math.min(
                (long) VD_SETTLE_RETRY_FIRST_SECONDS * (1L << Math.min(nextTimes - 1, 10)),
                VD_SETTLE_RETRY_MAX_BACKOFF_SECONDS);
        queue.add((double) (System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(delaySeconds)),
                buildVinSettleRetryMember(order.getCode(), order.getVinId(), nextTimes));
        log.warn("[VD结算重试] 第{}次重试未成功,{}s 后重试 code={} reason={}", nextTimes, delaySeconds, order.getCode(), reason);
    }
 
    private static int parseRetryTimes(String value) {
        try {
            return Integer.parseInt(value);
        } catch (Exception ex) {
            return 0;
        }
    }
 
    @Override
    public TVinChargingOrder getLatestStoppingByGunId(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getChargingGunId, gunId)
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .orderByDesc(TVinChargingOrder::getId)
                .last("limit 1"));
    }
 
    @Override
    public void vinTerminateStopByReply(PlatformStopChargingReplyVO vo) {
        // stopCharging 已前置更新状态4/结束方式1,此处仅兜底确认(幂等)
        if (vo == null) {
            return;
        }
        TChargingGun gun = null;
        try {
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(vo.getCharging_pile_code());
            gunQuery.setCharging_gun_code(vo.getCharging_gun_code());
            gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
        } catch (Exception ex) {
            log.error("[VD停止] 查询充电枪失败 pile={} gun={}", vo.getCharging_pile_code(), vo.getCharging_gun_code(), ex);
            return;
        }
        if (gun == null) {
            return;
        }
        TVinChargingOrder order = getLatestStoppingByGunId(gun.getId());
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(1);
        updateById(update);
        log.info("[VD停止] 平台停止应答兜底确认 code={}", order.getCode());
    }
 
    /**
     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
     */
    private void pushRegulatoryAsync(TVinChargingOrder order, boolean pushOrderStatus, boolean pushOrderInfo) {
        if (chargingMessageClient == null && chengDuJianGuanClient == null) {
            return;
        }
        new Thread(() -> {
            try {
                if (pushOrderStatus && chargingMessageClient != null) {
                    ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
                    // 与 CD 一致使用 Spring BeanUtils(本类 ruoyi BeanUtils 同名,故用全限定名)
                    org.springframework.beans.BeanUtils.copyProperties(order, chargingOrderVo);
                    chargingMessageClient.pushOrderStatus(chargingOrderVo);
                }
                if (chengDuJianGuanClient != null) {
                    NotificationConnectorChargeStatusVo statusVo = new NotificationConnectorChargeStatusVo();
                    statusVo.setOrderCode(order.getCode());
                    statusVo.setChargingGunId(order.getChargingGunId());
                    chengDuJianGuanClient.notificationConnectorChargeStatus(statusVo);
                    if (pushOrderInfo) {
                        chengDuJianGuanClient.notificationOrderInfo(statusVo);
                    }
                }
            } catch (Exception ex) {
                log.error("[VD监管] 推送监管平台失败 code={}", order.getCode(), ex);
            }
        }, "VD-Regulatory-Push").start();
    }
 
    private void resetGunQuietly(TVinChargingOrder order) {
        updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位");
    }
 
    private boolean updateGunStatusQuietly(TVinChargingOrder order, int status, String scene) {
        if (order.getChargingGunId() == null) {
            return true;
        }
        try {
            R<TChargingGun> queryResult = chargingGunClient.getChargingGunById(order.getChargingGunId());
            TChargingGun gun = queryResult == null ? null : queryResult.getData();
            if (queryResult == null || queryResult.getCode() != R.SUCCESS || gun == null) {
                log.error("[VD资源] {}查询充电枪失败 code={} gunId={}", scene, order.getCode(), order.getChargingGunId());
                return false;
            }
            gun.setStatus(status);
            R<String> result = chargingGunClient.updateChargingGunById(gun);
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[VD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(),
                        order.getChargingGunId(), result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[VD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex);
            return false;
        }
    }
 
    /**
     * 容错读取订单最早/最新监控记录的 SOC;Mongo 数据缺失或查询异常不阻断主结算流程。
     */
    private BigDecimal getMonitoringSocQuietly(String code, String sort) {
        if (monitoringDataService == null) {
            return null;
        }
        try {
            UploadRealTimeMonitoringData monitoringData = monitoringDataService.getDataByOrderCode(code, sort);
            return monitoringData == null || monitoringData.getSoc() == null
                    ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue());
        } catch (Exception ex) {
            log.error("[VD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex);
            return null;
        }
    }
 
    private LocalDateTime parseDeviceTime(String value, LocalDateTime fallback) {
        if (StringUtils.isEmpty(value)) {
            return fallback;
        }
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendPattern("yyyy-MM-dd HH:mm:ss")
                .optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd()
                .toFormatter();
        try {
            return LocalDateTime.parse(value.trim().replace('T', ' '), formatter);
        } catch (DateTimeParseException ex) {
            log.warn("[VD结算] 设备时间格式异常 value={}", value);
            return fallback;
        }
    }
 
    /**
     * 截取快照时段位 HH:mm(对齐 CD 充电 TaskUtil.endOrder 中 detailOrder.getStartTime().split(" ")[1];
     * 实现见公共支持类)
     */
    private static String splitPeriodHourMinute(String value) {
        return ChargeAccountSupport.splitPeriodHourMinute(value);
    }
 
    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
 
    /**
     * 判定该 VIN 是否存在未结账订单(启动中2/充电中3/停止中4):建单互斥使用。
     * 查询条件带 vin_id(单分片路由),且仅在 VIN 维度锁内调用,避免同 VIN 并发建单
     */
    private boolean existsUnsettledOrder(Long vinId) {
        if (vinId == null) {
            return false;
        }
        Long count = baseMapper.selectCount(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                        .eq(TVinChargingOrder::getVinId, vinId)
                        .in(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING,
                                ORDER_STATUS_STOPPING));
        return count != null && count > 0;
    }
 
    @Override
    public List<TVinChargingOrder> listTimeoutStartingOrders() {
        return lambdaQuery()
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(VD_STARTING_TIMEOUT_MINUTES))
                .list();
    }
 
    @Override
    public List<TVinChargingOrder> listTimeoutChargingOrders() {
        // DB 兜底扫描单轮上限 VD_DB_SCAN_LIMIT:结算失败回退的状态4订单优先由1分钟重试队列处理,
        // 未覆盖到的残留订单由后续轮次继续兜底
        return lambdaQuery()
                .in(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .last("limit " + VD_DB_SCAN_LIMIT)
                .list();
    }
 
    @Override
    public void closeTimeoutStartingOrder(TVinChargingOrder order) {
        TVinChargingOrder update = new TVinChargingOrder();
        update.setStatus(ORDER_STATUS_START_FAILED);
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getId, order.getId())
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
        if (!closed) {
            log.info("[VD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode());
            return;
        }
        // 释放申请时锁定的账户额度(未产生消费)
        unlockVinChargeContextQuietly(order, "启动关单");
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[VD补偿] 启动超时关单 code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
    }
 
    /**
     * 严格模式启动失败关单:仅关闭订单并释放锁定额度,不复位充电枪
     * (枪可能处于离线/故障等业务态,避免误复位为可用;订单已关单,不会残留占用)
     */
    private void closeStartingOrderWithoutGunReset(TVinChargingOrder order) {
        TVinChargingOrder update = new TVinChargingOrder();
        update.setStatus(ORDER_STATUS_START_FAILED);
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getId, order.getId())
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
        if (!closed) {
            log.info("[VD建单] 严格模式关单跳过,订单状态已变化 code={}", order.getCode());
            return;
        }
        // 释放申请时锁定的账户额度(未产生消费)
        unlockVinChargeContextQuietly(order, "启动关单");
        pushRegulatoryAsync(order, false, false);
        log.warn("[VD建单] 严格模式启动失败关单(不复位充电枪) code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
    }
 
    @Override
    public void compensationSettle(TVinChargingOrder order) {
        // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
        // (结算锁/状态幂等/枪复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
        BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
                : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
        int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId());
        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
        vo.setTransaction_serial_number(order.getCode());
        TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity);
        vo.setTotal_electricity(capacity);
        log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
                order.getCode(), flatPeriodIndex, capacity);
        vinEndChargeBillingCharge(vo);
    }
 
    private int resolveCompensationFlatPeriodIndex(Long orderId) {
        List<TVinAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(vinAccountingStrategyDetailOrderService
                        .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
                .orElse(Collections.emptyList()).stream()
                .filter(item -> item.getType() != null)
                .sorted(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
                        Comparator.nullsLast(String::compareTo)))
                .collect(Collectors.toList());
        for (int i = 0; i < snapshots.size(); i++) {
            if (Integer.valueOf(3).equals(snapshots.get(i).getType())) {
                return i + 1;
            }
        }
        log.error("[VD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
        return 1;
    }
}