puzhibing
5 小时以前 8ea02ca30907216aece38fda1ba918e5a5246abe
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
package com.ruoyi.order.service.impl;
 
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.ChargingCardAuthDTO;
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
import com.ruoyi.account.api.feignClient.AppUserCarClient;
import com.ruoyi.account.api.feignClient.ChargingCardClient;
import com.ruoyi.account.api.model.TAppUserCar;
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.model.TCompanyAllocateRecord;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
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.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.dto.TBillingTemplateDTO;
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.order.api.dto.DeviceStartChargingDTO;
import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder;
import com.ruoyi.order.api.model.TCardAccountingStrategyOrder;
import com.ruoyi.order.api.model.TCardChargingOrder;
import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.CardBalanceDetailQuery;
import com.ruoyi.order.api.query.CardChargingOrderQuery;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
import com.ruoyi.order.api.vo.CardBalanceDetailVO;
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.TransactionRecordMessageVO;
import com.ruoyi.order.mapper.TCardChargingOrderMapper;
import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TCardAccountingStrategyOrderService;
import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TCardChargingOrderService;
import com.ruoyi.chargingPile.api.model.Site;
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.UploadRealTimeMonitoringData;
import com.ruoyi.integration.api.vo.ChargingOrderVo;
import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
import com.ruoyi.order.util.TransactionStageParser;
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.CompanyPreferentialDTO;
import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import com.ruoyi.order.support.ChargeAccountSupport;
import org.redisson.api.RLock;
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.LocalDateTime;
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.Comparator;
import java.util.Date;
import java.util.Collections;
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 TCardChargingOrderServiceImpl extends ServiceImpl<TCardChargingOrderMapper, TCardChargingOrder>
        implements TCardChargingOrderService {
 
    private static final Logger log = LoggerFactory.getLogger(TCardChargingOrderServiceImpl.class);
 
    /** SD 订单号前缀(设备刷卡充电) */
    private static final String ORDER_CODE_PREFIX_SD = "SD";
    /** 订单状态:启动中 */
    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;
    /** SD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
    private static final String LOCK_SD_START_GUN = "SD_START_GUN:";
    /** SD 建单卡锁前缀(卡ID维度,防同卡多枪并发启动,文档13.2) */
    private static final String LOCK_SD_START_CARD = "SD_START_CARD:";
    /** SD 结算锁前缀(订单号维度,防并发重复结算/扣款,文档13.3) */
    private static final String LOCK_SD_SETTLE = "SD_SETTLE:";
    /** SD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
    private static final int SD_STARTING_TIMEOUT_MINUTES = 30;
    /** SD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
    private static final int SD_CHARGING_TIMEOUT_HOURS = 24;
    /** 状态5未扣款扫描保护窗,避免与正在提交的扣款事务竞态 */
    private static final int SD_DEDUCT_COMPENSATION_DELAY_MINUTES = 10;
    /** 单轮最多检查的已结束订单数,避免大批量远程查询 */
    private static final int SD_DEDUCT_COMPENSATION_BATCH_SIZE = 100;
 
    @Resource
    private TokenService tokenService;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private SiteClient siteClient;
    @Resource
    private ChargingPileClient chargingPileClient;
    @Resource
    private ChargingGunClient chargingGunClient;
    @Resource
    private AppUserCarClient appUserCarClient;
    @Resource
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private ChargingCardClient chargingCardClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private ChargeAuthorizationClient chargeAuthorizationClient;
    @Resource
    private ChargeAccountSupport chargeAccountSupport;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private AccountingStrategyDetailClient accountingStrategyDetailClient;
    @Resource
    private TCardAccountingStrategyOrderService cardAccountingStrategyOrderService;
    @Resource
    private TCardAccountingStrategyDetailOrderService cardAccountingStrategyDetailOrderService;
    @Resource
    private TCardChargingOrderAccountingStrategyService cardChargingOrderAccountingStrategyService;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private RedissonClient redissonClient;
 
    @Override
    public PageInfo<ChargingOrderVO> pageList(CardChargingOrderQuery query) {
        String[] timeRange = parseTimeRange(query);
        int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize();
        PageInfo<ChargingOrderVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        setAuthSiteIds(query);
        long offset = (long) (pageCurr - 1) * pageSize;
        long total = baseMapper.count(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        pageInfo.setTotal(total);
        List<ChargingOrderVO> list = total == 0 ? Collections.emptyList()
                : baseMapper.pageList(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3], offset, pageSize);
        fillPageExtra(list);
        pageInfo.setRecords(list);
        return pageInfo;
    }
 
    @Override
    public Map<String, Object> summary(CardChargingOrderQuery query) {
        String[] timeRange = parseTimeRange(query);
        setAuthSiteIds(query);
        Map<String, Object> result = baseMapper.summary(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        if (result == null) {
            result = new HashMap<>();
        }
        BigDecimal platformIncome = BigDecimal.ZERO;
        BigDecimal partnerIncome = BigDecimal.ZERO;
        Map<String, BigDecimal> accountingSummary = emptyAccountingSummary();
        List<Long> orderIds = baseMapper.summaryOrderIds(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        if (orderIds != null && !orderIds.isEmpty()) {
            int batchSize = 500;
            for (int i = 0; i < orderIds.size(); i += batchSize) {
                List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
                Map<String, Object> shareSummary = baseMapper.shareIncomeSummaryByOrderIds(batchIds);
                platformIncome = platformIncome.add(summaryDecimal(shareSummary, "platformIncome"));
                partnerIncome = partnerIncome.add(summaryDecimal(shareSummary, "partnerIncome"));
                mergeAccountingSummary(accountingSummary, baseMapper.accountingSummaryByOrderIds(batchIds));
            }
        }
        result.put("platformIncome", platformIncome);
        result.put("partnerIncome", partnerIncome);
        result.put("platformIncomeTotal", platformIncome);
        result.put("partnerIncomeTotal", partnerIncome);
        result.put("capacityProportion", buildProportion(accountingSummary, "capacity"));
        result.put("serviceProportion", buildProportion(accountingSummary, "service"));
        result.put("electronicProportion", buildProportion(accountingSummary, "electric"));
        return result;
    }
 
    private Map<String, BigDecimal> emptyAccountingSummary() {
        Map<String, BigDecimal> summary = new HashMap<>();
        for (String prefix : new String[]{"capacity", "service", "electric"}) {
            summary.put(prefix + "Total", BigDecimal.ZERO);
            for (int type = 1; type <= 4; type++) {
                summary.put(prefix + type, BigDecimal.ZERO);
            }
        }
        return summary;
    }
 
    private void mergeAccountingSummary(Map<String, BigDecimal> total, Map<String, Object> batch) {
        if (batch == null) {
            return;
        }
        for (String key : total.keySet()) {
            total.put(key, total.get(key).add(summaryDecimal(batch, key)));
        }
    }
 
    private BigDecimal summaryDecimal(Map<String, Object> summary, String key) {
        if (summary == null || summary.get(key) == null) {
            return BigDecimal.ZERO;
        }
        Object value = summary.get(key);
        return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
    }
 
    private String buildProportion(Map<String, BigDecimal> summary, String prefix) {
        BigDecimal denominator = summary.get(prefix + "Total");
        List<String> proportions = new ArrayList<>(4);
        for (int type = 1; type <= 4; type++) {
            BigDecimal percentage = denominator.signum() == 0 ? BigDecimal.ZERO
                    : summary.get(prefix + type).multiply(new BigDecimal("100"))
                    .divide(denominator, 2, RoundingMode.HALF_UP).stripTrailingZeros();
            proportions.add(percentage.toPlainString() + "%");
        }
        return String.join("-", proportions);
    }
 
    @Override
    public List<ChargingOrderVO> exportList(CardChargingOrderQuery query) {
        String[] timeRange = parseTimeRange(query);
        setAuthSiteIds(query);
        long total = baseMapper.count(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3]);
        List<ChargingOrderVO> result = new ArrayList<>();
        if (total == 0) {
            return result;
        }
        int batchSize = 5000;
        for (long offset = 0; offset < total; offset += batchSize) {
            List<ChargingOrderVO> batch = baseMapper.pageList(query, timeRange[0], timeRange[1], timeRange[2], timeRange[3], offset, batchSize);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        fillPageExtra(result);
        return result;
    }
 
    @Override
    public Map<String, Object> detail(Long chargingOrderId) {
        TCardChargingOrder order = getById(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", order.getId());
        result.put("uid", String.valueOf(order.getId()));
        result.put("code", order.getCode());
        result.put("cardNo", order.getCardNo());
        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());
        // 优惠金额(原始电费/服务费 - 最终,企业优惠设置改造)
        result.put("discountElectrovalence", order.getDiscountElectrovalence());
        result.put("discountServiceCharge", order.getDiscountServiceCharge());
        // 账户模式/授权截止时间/申请锁定额度(刷卡链路改造)
        result.put("accountMode", order.getAccountMode());
        // Map 值不走实体字段上的 @JsonFormat,显式格式化,避免前端展示 ISO 格式(含 T)
        result.put("maxEndTime", order.getMaxEndTime() == null ? null
                : order.getMaxEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("lockedAmount", order.getLockedAmount());
        result.put("total", defaultDecimal(order.getElectrovalence()).add(defaultDecimal(order.getServiceCharge())));
        result.put("realTotal", order.getPaymentAmount());
        result.put("endMode", order.getEndMode());
        result.put("phone", order.getPhone());
        result.put("licensePlate", order.getPlateNum());
        // SOC 展示口径:优先展示结算回填的开始/结束 SOC;历史订单未采集时保持占位符
        result.put("startSoc", order.getStartSoc() == null ? "-" : order.getStartSoc().toPlainString());
        result.put("endSoc", order.getEndSoc() == null ? "-" : order.getEndSoc().toPlainString());
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<UploadRealTimeMonitoringData> monitoring = order.getCode() == null || order.getCode().trim().isEmpty()
                ? Collections.emptyList()
                : uploadRealTimeMonitoringDataService.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));
            }
        }
        result.put("list", monitoring);
        UploadRealTimeMonitoringData latest = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
        result.put("cdElectronic", latest == null || latest.getOutput_current() == null ? "" : latest.getOutput_current().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("cdVoltage", latest == null || latest.getOutput_voltage() == null ? "" : latest.getOutput_voltage().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("totalPower", latest == null || latest.getPower() == null ? "" : latest.getPower().setScale(2, RoundingMode.HALF_DOWN).toPlainString());
        result.put("surplus", latest == null || latest.getSoc() == null ? "" : String.valueOf(latest.getSoc()));
        result.put("list1", baseMapper.accountingStrategyList(order.getId()));
        List<Integer> siteIds = order.getSiteId() == null ? Collections.emptyList() : Collections.singletonList(order.getSiteId());
        if (!siteIds.isEmpty()) {
            List<Site> sites = Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList());
            if (!sites.isEmpty()) {
                result.put("siteName", sites.get(0).getName());
                result.put("siteAddress", sites.get(0).getAddress());
            }
        }
        if (order.getChargingGunId() != null) {
            TChargingGun gun = chargingGunClient.getChargingGunById(order.getChargingGunId()).getData();
            if (gun != null) {
                result.put("gunCode", gun.getCode());
                result.put("terminalName", gun.getName());
            }
        }
        return result;
    }
 
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
 
    @Override
    public List<Map<String, Object>> balanceIncomeDetail(CardChargingOrderQuery query) {
        return baseMapper.balanceIncomeDetail(query);
    }
 
    @Override
    public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
        return baseMapper.accountingStrategyList(chargingOrderId);
    }
 
    /**
     * 管理后台停止刷卡充电:与 VIN 码停充口径一致,
     * 状态推进使用条件更新(仅当前状态为充电中时生效),防止重复点击/设备回调并发。
     */
    @Override
    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
        Long orderId;
        try {
            orderId = Long.valueOf(chargingOrderId);
        } catch (NumberFormatException e) {
            return AjaxResult.error("刷卡充电订单ID无效");
        }
        TCardChargingOrder order = getAuthorizedOrder(orderId);
        if (order == null) {
            return AjaxResult.error("刷卡充电订单不存在或无权操作");
        }
        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
            return AjaxResult.error("充电枪与订单不匹配");
        }
        Integer status = order.getStatus();
        if (status == null || status != ORDER_STATUS_CHARGING) {
            return AjaxResult.error(status != null && (status == ORDER_STATUS_STOPPING || status == ORDER_STATUS_FINISHED)
                    ? "不能重复操作" : "还未开始充电");
        }
        TChargingGun chargingGun;
        TChargingPile chargingPile;
        try {
            R<TChargingGun> gunResult = chargingGunClient.getChargingGunById(chargingGunId);
            chargingGun = gunResult == null ? null : gunResult.getData();
            R<TChargingPile> pileResult = order.getChargingPileId() == null ? null
                    : chargingPileClient.getChargingPileById(order.getChargingPileId());
            chargingPile = pileResult == null ? null : pileResult.getData();
        } catch (Exception ex) {
            log.error("[SD停充] 管理后台停充查询设备异常 code={} gunId={}", order.getCode(), chargingGunId, ex);
            return AjaxResult.error("查询充电设备失败");
        }
        if (chargingGun == null || chargingPile == null) {
            return AjaxResult.error("未找到订单对应的充电设备");
        }
        boolean updated = lambdaUpdate()
                .eq(TCardChargingOrder::getId, order.getId())
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .set(TCardChargingOrder::getEndMode, 1)
                .set(TCardChargingOrder::getEndTime, LocalDateTime.now())
                .update();
        if (!updated) {
            return AjaxResult.error("订单状态已变化,请刷新后重试");
        }
        order.setStatus(ORDER_STATUS_STOPPING);
        order.setEndMode(1);
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(chargingPile.getCode());
        command.setCharging_gun_code(chargingGun.getCode());
        sendMessageClient.platformStopCharging(command);
        log.warn("[SD停充] 管理后台主动停充 code={} gunId={}", order.getCode(), chargingGunId);
        return AjaxResult.success();
    }
 
    @Override
    public PageInfo<CardBalanceDetailVO> balanceDetailPage(CardBalanceDetailQuery 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<CardBalanceDetailVO> pageInfo = new PageInfo<>(pageCurr, pageSize);
        long total = baseMapper.balanceDetailCount(query);
        pageInfo.setTotal(total);
        if (total == 0) {
            pageInfo.setRecords(Collections.emptyList());
            return pageInfo;
        }
        long offset = (long) (pageCurr - 1) * pageSize;
        List<CardBalanceDetailVO> list = baseMapper.balanceDetailList(query, offset, pageSize);
        pageInfo.setRecords(list);
        return pageInfo;
    }
 
    @Override
    public Map<String, Object> balanceDetailSummary(CardBalanceDetailQuery query) {
        setAuthSiteIds(query);
        Map<String, Object> result = baseMapper.balanceDetailSummary(query);
        if (result == null) {
            result = new HashMap<>();
        }
        result.putIfAbsent("orderCount", 0);
        result.putIfAbsent("incomeAmount", BigDecimal.ZERO);
        result.putIfAbsent("expenseAmount", BigDecimal.ZERO);
        return result;
    }
 
    @Override
    public List<CardBalanceDetailVO> balanceDetailExport(CardBalanceDetailQuery query) {
        setAuthSiteIds(query);
        long total = baseMapper.balanceDetailCount(query);
        if (total == 0) {
            return new ArrayList<>();
        }
        List<CardBalanceDetailVO> result = new ArrayList<>();
        int batchSize = 5000;
        for (long offset = 0; offset < total; offset += batchSize) {
            List<CardBalanceDetailVO> batch = baseMapper.balanceDetailList(query, offset, batchSize);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        return result;
    }
 
    private void setAuthSiteIds(CardChargingOrderQuery query) {
        List<Integer> siteIds = new ArrayList<>();
        if (query.getSiteId() != null) {
            siteIds.add(query.getSiteId());
        } else {
            Long userId = tokenService.getLoginUser().getUserid();
            if (userId != null) {
                siteIds = Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()).orElse(Collections.emptyList())
                        .stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
            }
        }
        if (siteIds.isEmpty()) {
            siteIds.add(-1);
        }
        query.setSiteIds(siteIds);
    }
 
    /**
     * 按订单ID查询并校验数据范围(订单所属站点须在当前登录用户授权站点内),与 VIN 码停充校验口径一致
     */
    private TCardChargingOrder getAuthorizedOrder(Long chargingOrderId) {
        if (chargingOrderId == null) {
            return null;
        }
        TCardChargingOrder 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 void setAuthSiteIds(CardBalanceDetailQuery query) {
        List<Integer> siteIds = new ArrayList<>();
        Long userId = tokenService.getLoginUser().getUserid();
        if (userId != null) {
            siteIds = Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData()).orElse(Collections.emptyList())
                    .stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
        }
        if (siteIds.isEmpty()) {
            siteIds.add(-1);
        }
        query.setSiteIds(siteIds);
    }
 
    private void fillElectronicProportion(List<ChargingOrderVO> list) {
        List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
        if (orderIds.isEmpty()) {
            return;
        }
        List<Map<String, Object>> capacityRows = new ArrayList<>();
        int batchSize = 500;
        for (int i = 0; i < orderIds.size(); i += batchSize) {
            List<Long> batchIds = orderIds.subList(i, Math.min(i + batchSize, orderIds.size()));
            capacityRows.addAll(Optional.ofNullable(baseMapper.capacitySummaryByOrderIds(batchIds))
                    .orElse(Collections.emptyList()));
        }
        Map<Long, Map<String, Object>> capacityByOrderId = capacityRows.stream()
                .filter(item -> item.get("orderId") != null)
                .collect(Collectors.toMap(item -> Long.valueOf(item.get("orderId").toString()), item -> item, (a, b) -> a));
        for (ChargingOrderVO order : list) {
            Map<String, Object> capacity = capacityByOrderId.get(order.getId());
            Map<String, BigDecimal> summary = emptyAccountingSummary();
            if (capacity != null) {
                for (String key : new String[]{"capacityTotal", "capacity1", "capacity2", "capacity3", "capacity4"}) {
                    summary.put(key, summaryDecimal(capacity, key));
                }
            }
            order.setElectronicProportion(buildProportion(summary, "capacity"));
        }
    }
 
    private void fillPageExtra(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        fillElectronicProportion(list);
        fillCompanyFleetNames(list);
        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());
        List<Long> carIds = list.stream().map(ChargingOrderVO::getAppUserCarId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, TAppUserCar> cars = carIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(appUserCarClient.getCarByIds(carIds).getData()).orElse(Collections.emptyList()).stream()
                        .collect(Collectors.toMap(TAppUserCar::getId, item -> item, (a, b) -> a));
        Map<Integer, Site> sites = 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());
            TAppUserCar car = cars.get(item.getAppUserCarId());
            item.setVin(car == null ? null : car.getVin());
            if (item.getStartTime() != null && item.getEndTime() != null) {
                item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime()));
            }
            UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getLastDataById(item.getCode());
            if (monitoringData != null) {
                item.setChargeCurrent(monitoringData.getOutput_current());
                item.setChargeVoltage(monitoringData.getOutput_voltage());
                item.setChargePower(monitoringData.getPower());
            }
            item.setPlatformIncome(BigDecimal.ZERO);
            item.setPartnerIncome(BigDecimal.ZERO);
        }
    }
 
    /**
     * 回填所属公司/车队名称:按订单快照 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("[SD回填] 批量查询公司名称失败 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("[SD回填] 批量查询车队名称失败 ids={}", fleetIds, e);
        }
        return result;
    }
 
    private String[] parseTimeRange(CardChargingOrderQuery query) {
        return new String[]{query.getStartTime1(), query.getStartTime2(), query.getEndTime1(), query.getEndTime2()};
    }
 
    // ===== 设备侧刷卡充电链路(V0.2 刷卡充电) =====
 
    @Override
    public DeviceStartChargingVO startCardCharging(DeviceStartChargingDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
            return DeviceStartChargingVO.fail(1);
        }
        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
        RLock gunLock = redissonClient.getLock(LOCK_SD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
        try {
            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
                return DeviceStartChargingVO.fail(10);
            }
            // 1. 充电枪校验(不存在->5 桩停用;充电中->10 该桩存在未结账记录)
            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. 卡片鉴权(卡号存在性/禁用/密码条件校验在账户服务内完成)
            ChargingCardAuthDTO authDTO = new ChargingCardAuthDTO();
            authDTO.setCardNo(dto.getAccount().trim());
            authDTO.setPassword(dto.getPassword());
            R<TChargingCard> verifyResult = chargingCardClient.verifyCharging(authDTO);
            TChargingCard card = verifyResult.getData();
            if (card == null) {
                return DeviceStartChargingVO.fail(mapAuthFailure(verifyResult.getMsg()));
            }
            // 3. 计费策略(枪策略优先,站点策略兜底):授权校验(指定电价时段)与策略快照共用
            Integer strategyId = gun.getAccountingStrategyId() != null
                    ? gun.getAccountingStrategyId()
                    : (site == null ? null : site.getAccountingStrategyId());
            // 3.1 账户上下文解析(企业状态、充电授权站点范围/可用时段、账户模式、可用余额、授权截止时间)
            ChargeAccountSupport.ChargeContext chargeContext = resolveCardChargeContext(gun.getSiteId(), strategyId, card);
            if (!chargeContext.isOk()) {
                log.warn("[SD建单] 账户校验不通过 cardId={} gun={} reason={}", card.getId(),
                        dto.getChargingGunCode(), chargeContext.getFailReason());
                return DeviceStartChargingVO.fail(chargeContext.getFailCode());
            }
 
            // 并发防护(文档13.2):卡ID维度锁,防同卡多枪并发建单(数据库锁卡条件更新为第二道保护)
            RLock cardLock = redissonClient.getLock(LOCK_SD_START_CARD + card.getId());
            boolean cardLocked = false;
            try {
                cardLocked = cardLock.tryLock(0, 30, TimeUnit.SECONDS);
                if (!cardLocked) {
                    return DeviceStartChargingVO.fail(4);
                }
                // 4. 建单(状态2 启动中)
                TCardChargingOrder order = new TCardChargingOrder();
                order.setCode(ORDER_CODE_PREFIX_SD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
                order.setOrderType(2);
                order.setOrderSource(1);
                order.setOrderClassification(2);
                // 充电支付模式(沿用 CD payMethod 枚举:1=余额支付):SD 全程卡余额扣款
                order.setPayMethod(1);
                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.setCardId(card.getId());
                order.setCardNo(card.getCardNo());
                // 所属公司/车队快照落库(充电卡当前归属),供订单列表/汇总/导出按公司、车队直接筛选
                order.setCompanyId(card.getCompanyId());
                order.setFleetId(card.getFleetId());
                // 可用金额基数与启动剩余金额:共享模式=min(授权下发额, 企业可用余额),分配模式=min(授权下发额, 卡可用余额)
                order.setChargeAmount(chargeContext.getDispatchAmount());
                order.setResidualAmount(chargeContext.getDispatchAmount());
                // 账户模式与授权截止时间快照(锁定额度在锁定成功后回填,与 VIN 链路一致)
                order.setAccountMode(chargeContext.getAccountMode());
                order.setMaxEndTime(chargeContext.getMaxEndTime());
                order.setCreateTime(LocalDateTime.now());
                save(order);
 
                // 5. 计费策略快照(枪策略优先,站点策略兜底,strategyId 在授权校验前已解析):
                //    无可用策略(未配置/已被删除、明细为空)或保存异常时无法计费,直接回滚删除订单并回告启动失败,
                //    避免产生无策略订单(结算侧的价格兜底仅用于历史订单)
                try {
                    if (!saveStrategySnapshot(order.getId(), strategyId)) {
                        log.warn("[SD建单] 无可用计费策略,回滚删除订单 code={} strategyId={}", order.getCode(), strategyId);
                        removeOrderWithSnapshot(order.getId());
                        return DeviceStartChargingVO.fail(null);
                    }
                } catch (Exception snapshotEx) {
                    log.error("[SD建单] 保存计费策略快照异常,回滚删除订单 orderId={} strategyId={}", order.getId(),
                            strategyId, snapshotEx);
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(null);
                }
 
                // 6. 锁定账户额度(实扣冻结:共享=企业账户,分配=卡账户;锁定失败则关单并回告余额不足)
                if (!lockCardChargeContext(card, chargeContext)) {
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(3);
                }
                // 6.1 回填锁定额度(订单列表/详情展示与结算释放使用),同步内存对象,保证后续回滚能按额度释放
                TCardChargingOrder accountUpdate = new TCardChargingOrder();
                accountUpdate.setId(order.getId());
                accountUpdate.setLockedAmount(chargeContext.getDispatchAmount());
                // 企业优惠快照落库(建单时固化,结算/补偿优先读快照,抵抗公司优惠设置后续变更)
                chargeAccountSupport.fillPreferentialSnapshot(accountUpdate, card.getCompanyId());
                updateById(accountUpdate);
                order.setLockedAmount(chargeContext.getDispatchAmount());
 
                // 7. 锁卡(条件更新:仅空闲卡可锁;失败释放账户锁定额并回滚删除订单与快照)
                R<Boolean> lockResult = chargingCardClient.lockForCharging(card.getId(), order.getCode());
                if (!Boolean.TRUE.equals(lockResult.getData())) {
                    log.warn("[SD建单] 锁卡失败回滚订单 code={} cardId={} msg={}", order.getCode(), card.getId(), lockResult.getMsg());
                    unlockCardChargeContextQuietly(order, "锁卡失败");
                    removeOrderWithSnapshot(order.getId());
                    return DeviceStartChargingVO.fail(4);
                }
                log.info("[SD建单] 刷卡充电订单创建成功 code={} cardNo={} gun={} accountMode={} lockAmount={} maxEndTime={}",
                        order.getCode(), card.getCardNo(), dto.getChargingGunCode(), chargeContext.getAccountMode(),
                        chargeContext.getDispatchAmount(), chargeContext.getMaxEndTime());
                return DeviceStartChargingVO.success(order.getCode(), chargeContext.getDispatchAmount());
            } finally {
                if (cardLocked && cardLock.isHeldByCurrentThread()) {
                    cardLock.unlock();
                }
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[SD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
            return DeviceStartChargingVO.fail(null);
        } catch (Exception e) {
            log.error("[SD建单] 刷卡充电启动异常 pile={} gun={} account={}",
                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
            return DeviceStartChargingVO.fail(null);
        } finally {
            if (gunLock.isHeldByCurrentThread()) {
                gunLock.unlock();
            }
        }
    }
 
    /**
     * 刷卡充电账户上下文解析(与 VIN 链路同构):
     * 1) 企业状态校验(未归属/不存在/停用 -> 失败码2),账户模式取企业配置(默认共享);
     * 2) 企业充电授权校验:站点范围(超范围 -> 失败码2)、可用时段(不在时段 -> 失败码2),未配置授权按放行处理;
     * 3) 可用余额校验(共享=企业可用余额,分配=卡可用余额,<=0 -> 失败码3);
     * 4) 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定额度与下发金额一致;授权截止时间用于监控提前停充。
     */
    private ChargeAccountSupport.ChargeContext resolveCardChargeContext(Integer siteId, Integer strategyId,
                                                                       TChargingCard card) {
        // 1. 企业校验:刷卡充电按企业账户模式计费,卡必须归属有效企业
        Integer companyId = card.getCompanyId();
        if (companyId == null) {
            return ChargeAccountSupport.ChargeContext.fail(2, "卡片未归属企业");
        }
        TGroupCompany company;
        try {
            R<TGroupCompany> companyResult = groupCompanyClient.getCompanyById(companyId);
            company = companyResult == null ? null : companyResult.getData();
        } catch (Exception ex) {
            log.error("[SD建单] 查询企业失败 companyId={}", companyId, ex);
            return ChargeAccountSupport.ChargeContext.fail(2, "企业信息查询失败");
        }
        if (company == null) {
            return ChargeAccountSupport.ChargeContext.fail(2, "企业不存在");
        }
        if (company.getStatus() != null && company.getStatus() != 1) {
            return ChargeAccountSupport.ChargeContext.fail(2, "企业已停用");
        }
        int accountMode = company.getAccountMode() == null
                ? ChargeAccountSupport.ACCOUNT_MODE_SHARED : company.getAccountMode();
 
        // 2. 充电授权校验(站点范围 + 可用时段 + 授权截止时间)
        ChargeAuthorizationDTO authorization = null;
        LocalDateTime maxEndTime = null;
        try {
            R<ChargeAuthorizationDTO> authResult = chargeAuthorizationClient.getDetailByCompanyId(companyId);
            authorization = authResult == null ? null : authResult.getData();
        } catch (Exception ex) {
            log.error("[SD建单] 查询企业充电授权失败 companyId={}", companyId, ex);
        }
        if (authorization != null) {
            List<Integer> siteIds = authorization.getSiteIds();
            if (siteIds != null && !siteIds.isEmpty() && siteId != null && !siteIds.contains(siteId)) {
                return ChargeAccountSupport.ChargeContext.fail(2, "站点不在企业充电授权范围");
            }
            ChargeAccountSupport.AuthorizationWindow window =
                    chargeAccountSupport.resolveAuthorizationWindow(authorization, strategyId, companyId);
            if (window == null) {
                return ChargeAccountSupport.ChargeContext.fail(2, "当前时间不在企业充电授权时段");
            }
            maxEndTime = window.getMaxEndTime();
        }
 
        // 3. 可用余额校验(实扣冻结口径,可用余额<=0 不允许启动)
        BigDecimal usable = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(accountMode)
                ? card.getBalance() : company.getAvailableBalance();
        usable = usable == null ? BigDecimal.ZERO : usable;
        if (usable.compareTo(BigDecimal.ZERO) <= 0) {
            return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足");
        }
 
        // 4. 下发金额:共享模式=min(授权下发额, 可用余额),分配模式=卡账户余额;锁定金额与下发金额一致
        BigDecimal dispatchAmount;
        if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(accountMode)) {
            dispatchAmount = usable;
        } else {
            dispatchAmount = authorization == null || authorization.getDispatchAmount() == null
                    ? ChargeAccountSupport.DEFAULT_DISPATCH_AMOUNT : authorization.getDispatchAmount();
            if (dispatchAmount.compareTo(usable) > 0) {
                dispatchAmount = usable;
            }
        }
        if (dispatchAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return ChargeAccountSupport.ChargeContext.fail(3, "账户可用余额不足");
        }
        return ChargeAccountSupport.ChargeContext.ok(accountMode, dispatchAmount, maxEndTime);
    }
 
    /**
     * 锁定账户额度(实扣冻结):共享模式锁企业账户,分配模式锁卡账户;锁定失败回告余额不足
     */
    private boolean lockCardChargeContext(TChargingCard card, ChargeAccountSupport.ChargeContext context) {
        BigDecimal amount = context.getDispatchAmount();
        try {
            R<BigDecimal> lockResult;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(context.getAccountMode())) {
                lockResult = chargingCardClient.lockBalance(card.getId(), amount);
            } else {
                lockResult = groupCompanyClient.lockBalance(card.getCompanyId(), amount);
            }
            if (lockResult == null || lockResult.getCode() != R.SUCCESS) {
                log.warn("[SD建单] 锁定账户额度失败 cardId={} companyId={} accountMode={} amount={} msg={}",
                        card.getId(), card.getCompanyId(), context.getAccountMode(), amount,
                        lockResult == null ? "null" : lockResult.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[SD建单] 锁定账户额度异常 cardId={} companyId={} amount={}", card.getId(), card.getCompanyId(),
                    amount, ex);
            return false;
        }
    }
 
    /**
     * 释放账户锁定额(建单失败/回滚/关单场景):共享=企业账户,分配=卡账户;
     * 订单侧 lock_released 条件更新抢占,保证同一订单的锁定额在多路径/重复补偿下只释放一次;
     * 释放失败时把标记回滚为未释放,留待补偿任务/人工重试
     */
    private void unlockCardChargeContextQuietly(TCardChargingOrder order, String scene) {
        if (order == null || order.getId() == null || order.getCompanyId() == null) {
            return;
        }
        BigDecimal amount = order.getLockedAmount();
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        boolean claimed;
        try {
            claimed = lambdaUpdate()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getLockReleased, 0)
                    .set(TCardChargingOrder::getLockReleased, 1)
                    .update();
        } catch (Exception ex) {
            log.error("[SD建单] 抢占锁定额释放标记异常 scene={} code={}", scene, order.getCode(), ex);
            return;
        }
        if (!claimed) {
            log.info("[SD建单] 锁定额已释放,跳过重复释放 scene={} code={}", scene, order.getCode());
            return;
        }
        boolean unlocked = false;
        try {
            R<BigDecimal> unlockResult;
            if (Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode())) {
                unlockResult = chargingCardClient.unlockBalance(order.getCardId(), amount);
            } else {
                unlockResult = groupCompanyClient.unlockBalance(order.getCompanyId(), amount);
            }
            unlocked = unlockResult != null && unlockResult.getCode() == R.SUCCESS;
            if (!unlocked) {
                log.error("[SD建单] 释放账户锁定额失败 scene={} code={} amount={} msg={}", scene, order.getCode(),
                        amount, unlockResult == null ? "null" : unlockResult.getMsg());
            }
        } catch (Exception ex) {
            log.error("[SD建单] 释放账户锁定额异常 scene={} code={} amount={}", scene, order.getCode(), amount, ex);
        }
        if (!unlocked) {
            lambdaUpdate()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getLockReleased, 1)
                    .set(TCardChargingOrder::getLockReleased, 0)
                    .update();
        }
    }
 
    /**
     * 回滚删除订单及其计费策略快照(建单失败场景;快照可能尚未写入,删除为幂等操作)
     */
    private void removeOrderWithSnapshot(Long orderId) {
        if (orderId == null) {
            return;
        }
        try {
            cardAccountingStrategyDetailOrderService.remove(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyDetailOrder>()
                            .eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId));
            cardAccountingStrategyOrderService.remove(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardAccountingStrategyOrder>()
                            .eq(TCardAccountingStrategyOrder::getChargingOrderId, orderId));
        } catch (Exception ex) {
            log.error("[SD建单] 清理计费策略快照异常 orderId={}", orderId, ex);
        }
        removeById(orderId);
    }
 
    /**
     * 保存计费策略快照(主表 + 明细表),与 CD 订单快照逻辑一致
     *
     * @return 是否成功写入快照(策略ID为空、策略/明细不存在或明细为空、持久化失败均返回 false,调用方需回滚建单)
     */
    private boolean saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
            return false;
        }
        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
        if (strategy == null || details == null || details.isEmpty()) {
            return false;
        }
        TCardAccountingStrategyOrder strategyOrder = new TCardAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setChargingOrderId(orderId);
        boolean mainSaved = cardAccountingStrategyOrderService.save(strategyOrder);
        List<TCardAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
        for (TAccountingStrategyDetail detail : details) {
            TCardAccountingStrategyDetailOrder detailOrder = new TCardAccountingStrategyDetailOrder();
            BeanUtils.copyProperties(detail, detailOrder);
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        boolean detailSaved = cardAccountingStrategyDetailOrderService.saveBatch(detailOrders);
        return mainSaved && detailSaved;
    }
 
    /**
     * 账户服务鉴权失败消息 -> 协议失败原因码(1=账户不存在 2=账户冻结 7=密码错误)
     */
    private Integer mapAuthFailure(String msg) {
        if (StringUtils.isNotEmpty(msg)) {
            if (msg.contains("密码")) {
                return 7;
            }
            if (msg.contains("禁用") || msg.contains("冻结")) {
                return 2;
            }
            if (msg.contains("不存在")) {
                return 1;
            }
        }
        return 1;
    }
 
    @Override
    public TCardChargingOrder getCardOrderByCode(String code) {
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TCardChargingOrder>()
                .eq(TCardChargingOrder::getCode, code)
                .last("limit 1"));
    }
 
    @Override
    public void cardStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(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(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                    .set(TCardChargingOrder::getStartTime, LocalDateTime.now())
                    .update();
            if (!updated) {
                return;
            }
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功");
            log.info("[SD启动] 刷卡订单进入充电中 code={}", order.getCode());
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
        } else {
            closeTimeoutStartingOrder(order);
            log.warn("[SD启动] 刷卡订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
        }
    }
 
    @Override
    public boolean handleHandshakeVin(String orderCode, String vin) {
        if (StringUtils.isEmpty(orderCode)) {
            return false;
        }
        TCardChargingOrder order = getCardOrderByCode(orderCode);
        if (order == null) {
            log.warn("[SD握手] 订单不存在 code={}", orderCode);
            return false;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        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<TCardChargingOrder>()
                    .eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::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("[SD握手] 握手推进订单进入充电中 code={}", orderCode);
        }
        return updated;
    }
 
    @Override
    public void cardChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(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;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        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());
        }
        // 剩余金额口径与 CD 一致:可用金额基数(建单写入 charge_amount) - 设备已充金额
        if (query.getPaid_amount() != null && order.getChargeAmount() != null) {
            update.setResidualAmount(order.getChargeAmount().subtract(query.getPaid_amount())
                    .setScale(2, RoundingMode.HALF_EVEN));
        }
        boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TCardChargingOrder>()
                .eq(TCardChargingOrder::getId, order.getId())
                .in(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING));
        if (starting && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复");
        }
    }
 
    /**
     * 判断是否触发授权时间到期主动停充(提前 AUTHORIZE_STOP_AHEAD_SECONDS 秒下发,规避数据传输延迟):
     * 授权总时长不足固定提前量时按整个授权时长提前(启动后即触发停充),确保授权截止时间前结束充电;
     * maxEndTime 为空(全天授权/未开启自动停充)时不介入。
     */
    private boolean needAuthorizeStop(TCardChargingOrder order) {
        LocalDateTime startTime = order.getStartTime() != null ? order.getStartTime() : order.getCreateTime();
        return chargeAccountSupport.needAuthorizeStop(order.getMaxEndTime(), startTime);
    }
 
    /**
     * 授权时间到期主动停充:复用小程序主动停充链路(状态4 + 结束方式1 + 下发停止充电命令),
     * 状态推进使用条件更新,与设备回调/其他停充入口并发时只有一方生效
     */
    private void stopByAuthorizeExpired(TCardChargingOrder 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("[SD停充] 授权到期停充查询设备异常 code={}", order.getCode(), ex);
            return;
        }
        if (gun == null || pile == null) {
            log.error("[SD停充] 授权到期停充缺少设备信息 code={} gunId={}", order.getCode(), order.getChargingGunId());
            return;
        }
        boolean updated = lambdaUpdate()
                .eq(TCardChargingOrder::getId, order.getId())
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .set(TCardChargingOrder::getEndMode, 1)
                .set(TCardChargingOrder::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("[SD停充] 授权时间到期主动停充 code={} maxEndTime={}", order.getCode(), order.getMaxEndTime());
    }
 
    @Override
    public void cardEndCharge(String orderCode, Integer endMode) {
        if (StringUtils.isEmpty(orderCode)) {
            return;
        }
        TCardChargingOrder order = getCardOrderByCode(orderCode);
        if (order == null) {
            return;
        }
        TCardChargingOrder update = new TCardChargingOrder();
        update.setId(order.getId());
        update.setEndMode(endMode);
        updateById(update);
        pushRegulatoryAsync(order, false, false);
    }
 
    @Override
    public R cardEndChargeBillingCharge(TransactionRecordMessageVO vo) {
        String code = vo == null ? null : vo.getTransaction_serial_number();
        log.info("[SD结算] 收到交易记录 code={}", code);
        if (StringUtils.isEmpty(code)) {
            return R.fail("交易记录缺少交易流水号");
        }
        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等与扣款唯一键两层兜底
        RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + code);
        try {
            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.warn("[SD结算] 并发结算进行中,跳过重复账单 code={}", code);
                return R.ok("重复账单,结算处理中");
            }
            return doCardEndChargeBillingCharge(vo, code);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return R.fail("结算锁获取被中断:" + code);
        } finally {
            if (settleLock.isHeldByCurrentThread()) {
                settleLock.unlock();
            }
        }
    }
 
    /**
     * SD 结算实际执行体(调用前须持有订单号结算锁)
     */
    private R doCardEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
        TCardChargingOrder order = getCardOrderByCode(code);
        if (order == null) {
            return R.fail("刷卡充电订单不存在:" + code);
        }
        // 幂等:已结算订单直接返回成功(重复账单/重复推送)
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
            log.warn("[SD结算] 重复账单跳过 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<TCardAccountingStrategyDetailOrder> snapshots = new ArrayList<>(cardAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list());
        snapshots.removeIf(item -> item.getType() == null);
        snapshots.sort(Comparator.comparing(TCardAccountingStrategyDetailOrder::getStartTime,
                Comparator.nullsLast(String::compareTo)));
 
        // 企业优惠设置与计费模板(1=不优惠,2=服务费按比例优惠,3=按计费模板优惠单价收费):
        // 订单快照优先(建单时固化,策略后续变更不影响本单结算口径),快照为空的历史订单实时查询并在本事务回填
        ChargeAccountSupport.PreferentialSnapshot preferential = chargeAccountSupport.resolvePreferential(order);
        TBillingTemplateDTO billingTemplate = preferential == null ? null : preferential.getTemplate();
 
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        BigDecimal discountElectricTotal = BigDecimal.ZERO;
        BigDecimal discountServiceTotal = BigDecimal.ZERO;
        List<TCardChargingOrderAccountingStrategy> details = new ArrayList<>();
        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
            TCardAccountingStrategyDetailOrder 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(ChargeAccountSupport.PREFERENTIAL_TYPE_RATE).equals(preferential.getPreferentialType())) {
                finalServicePrice = ChargeAccountSupport.applyServiceDiscountRate(servicePrice, preferential.getServiceDiscountRate());
            } else if (preferential != null
                    && Integer.valueOf(ChargeAccountSupport.PREFERENTIAL_TYPE_TEMPLATE).equals(preferential.getPreferentialType())) {
                TBillingTemplateDetail templateDetail = ChargeAccountSupport.findTemplateDetail(billingTemplate, snapshot.getType());
                if (templateDetail == null) {
                    log.warn("[SD结算] 计费模板缺少对应阶段,该段按原价结算 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);
 
            TCardChargingOrderAccountingStrategy detail = new TCardChargingOrderAccountingStrategy();
            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("[SD结算] 订单无计费策略快照,按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;
        // SOC 回填(与 VIN 结算同口径):取订单最早/最新实时监控上报,读取失败不阻断结算
        final BigDecimal startSoc = getMonitoringSocQuietly(code, "asc");
        final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc");
        // 剩余金额口径:可用金额基数(建单写入的 charge_amount) - 本单金额;
        // 改造前创建的老订单无基数时以本单金额兜底(剩余金额记 0),并顺带补齐 charge_amount
        final BigDecimal chargeBase = order.getChargeAmount() != null ? order.getChargeAmount() : orderAmount;
        final BigDecimal residualAmount = chargeBase.subtract(orderAmount).setScale(2, RoundingMode.HALF_EVEN);
        final ChargeAccountSupport.PreferentialSnapshot preferentialSnapshot = preferential;
        final boolean preferentialBackfill = preferentialSnapshot != null && preferentialSnapshot.needBackfill();
 
        // 本地事务:写计费明细 + 更新订单(状态5,允许负余额金额记录)
        transactionTemplate.execute(status -> {
            cardChargingOrderAccountingStrategyService.lambdaUpdate()
                    .eq(TCardChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
                    .remove();
            if (!details.isEmpty()) {
                cardChargingOrderAccountingStrategyService.saveBatch(details);
            }
            TCardChargingOrder update = new TCardChargingOrder();
            update.setId(order.getId());
            update.setTotalElectricity(totalElec);
            update.setChargingCapacity(totalElec);
            update.setElectrovalence(elecTotal);
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            // 优惠合计(原始电费/服务费 - 最终),与 VIN 链路同口径
            update.setDiscountElectrovalence(discountElecTotal);
            update.setDiscountServiceCharge(discountSvcTotal);
            // 余额支付金额(本单实际余额扣款额,供余额明细"充电余额扣款"与支付方式展示)
            update.setBalancePayAmount(orderAmount);
            update.setChargeAmount(chargeBase);
            update.setResidualAmount(residualAmount);
            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);
            }
            // SOC 回填:监控数据缺失/查询异常时不覆盖既有值,与 VIN 结算行为一致
            if (startSoc != null) {
                update.setStartSoc(startSoc);
            }
            if (endSoc != null) {
                update.setEndSoc(endSoc);
            }
            if (preferentialBackfill) {
                // 历史订单(快照为空)实时查询结果回填订单快照列,保证后续补偿重算口径一致
                update.setPreferentialType(preferentialSnapshot.getPreferentialType());
                update.setServiceDiscountRate(preferentialSnapshot.getServiceDiscountRate());
                update.setBillingTemplateId(preferentialSnapshot.getBillingTemplateId());
                update.setBillingTemplateSnapshot(
                        ChargeAccountSupport.serializeTemplateSnapshot(preferentialSnapshot.getTemplate()));
                update.setPreferentialSnapshotTime(LocalDateTime.now());
            }
            updateById(update);
            return true;
        });
 
        // 回写内存订单对象,保证结算后监管推送/日志使用最新金额字段
        order.setChargeAmount(chargeBase);
        order.setResidualAmount(residualAmount);
        order.setBalancePayAmount(orderAmount);
        order.setOrderAmount(orderAmount);
        order.setPaymentAmount(orderAmount);
        order.setDiscountElectrovalence(discountElectricTotal);
        order.setDiscountServiceCharge(discountServiceTotal);
 
        // 账户结算(实扣冻结口径):解锁申请时锁定的额度、按实际消费扣款并退回差额,同时写分配记录消费流水;
        // 无法确认扣款成功时回退状态4,交由补偿任务重试
        if (!settleCardChargeAccount(order, orderAmount, code)) {
            rollbackFinishedToStopping(order.getId(), code);
            return R.fail("充电卡结算失败,订单已进入补偿队列");
        }
 
        // 结算成功后解锁卡片
        unlockCardQuietly(order);
        // 复位充电枪
        resetGunQuietly(order);
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[SD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={} discountElec={} discountSvc={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount, discountElectricTotal,
                discountServiceTotal);
        return R.ok();
    }
 
    /**
     * 容错读取订单最早/最新监控记录的 SOC(与 VIN 结算同口径):
     * Mongo 数据缺失、SOC 未上报或查询异常返回 null,不阻断主结算流程。
     */
    private BigDecimal getMonitoringSocQuietly(String code, String sort) {
        if (uploadRealTimeMonitoringDataService == null) {
            return null;
        }
        try {
            UploadRealTimeMonitoringData monitoringData = uploadRealTimeMonitoringDataService.getDataByOrderCode(code, sort);
            return monitoringData == null || monitoringData.getSoc() == null
                    ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue());
        } catch (Exception ex) {
            log.error("[SD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex);
            return null;
        }
    }
 
    /**
     * 结算刷卡账户资金(实扣冻结口径,与 VIN 链路同构):
     * 1) 分配模式:解锁卡片锁定额并按实际消费扣减(允许扣成负数),差额退回卡账户;
     * 2) 共享模式:解锁企业账户锁定额并按实际消费扣减,差额退回企业账户;
     * 3) 写入企业额度分配记录-消费流水(按订单号+change_type 幂等)。
     * 改造前的历史订单(无锁定金额)沿用历史扣款接口扣卡余额,保持既有行为不变。
     *
     * @return 是否确认结算成功(false 表示需回退状态等待补偿重试)
     */
    private boolean settleCardChargeAccount(TCardChargingOrder order, BigDecimal actualAmount, String code) {
        BigDecimal lockedAmount = order.getLockedAmount() == null ? BigDecimal.ZERO : order.getLockedAmount();
        BigDecimal actual = actualAmount == null ? BigDecimal.ZERO : actualAmount;
        if (actual.compareTo(BigDecimal.ZERO) <= 0 && lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            log.info("[SD结算] 订单金额为0且无锁定额度,跳过账户结算 code={}", code);
            return true;
        }
        boolean allocated = Integer.valueOf(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED).equals(order.getAccountMode());
        if (lockedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            // 历史订单(改造前无锁定金额):沿用仅按实扣金额扣卡余额的旧接口
            log.info("[SD结算] 订单无锁定金额,按历史扣款接口结算 code={} accountMode={}", code, order.getAccountMode());
            return legacyCardDeduct(order, actual, code);
        }
        BigDecimal afterBalance;
        try {
            if (allocated) {
                ChargingCardDeductDTO settleDTO = new ChargingCardDeductDTO();
                settleDTO.setCardId(order.getCardId());
                settleDTO.setOrderCode(code);
                settleDTO.setAmount(actual);
                settleDTO.setLockedAmount(lockedAmount);
                R<ChargingCardDeductVO> settleResult = chargingCardClient.settleForCharging(settleDTO);
                if (settleResult == null || settleResult.getCode() != R.SUCCESS || settleResult.getData() == null) {
                    log.error("[SD结算] 卡账户结算失败 code={} cardId={} locked={} actual={} msg={}", code,
                            order.getCardId(), lockedAmount, actual,
                            settleResult == null ? "null" : settleResult.getMsg());
                    return false;
                }
                if (settleResult.getData().isDuplicated()) {
                    log.warn("[SD结算] 卡账户结算幂等命中(重复请求不重复扣)code={}", code);
                }
                afterBalance = settleResult.getData().getAfterBalance();
            } else {
                // 共享模式:传订单号启用账户侧结算幂等(账户侧按订单号+变更类型落企业消费流水并作为已结算标记)
                R<BigDecimal> settleResult =
                        groupCompanyClient.settleBalance(order.getCompanyId(), lockedAmount, actual,
                                code, order.getCardNo(), null, "刷卡充电结算");
                if (settleResult == null || settleResult.getCode() != R.SUCCESS) {
                    log.error("[SD结算] 企业账户结算失败 code={} companyId={} locked={} actual={} msg={}", code,
                            order.getCompanyId(), lockedAmount, actual,
                            settleResult == null ? "null" : settleResult.getMsg());
                    return false;
                }
                afterBalance = settleResult.getData();
            }
        } catch (Exception ex) {
            log.error("[SD结算] 账户结算异常 code={} accountMode={} locked={} actual={}", code, order.getAccountMode(),
                    lockedAmount, actual, ex);
            return false;
        }
        if (allocated) {
            // 分配模式消费流水记卡账户;共享模式消费流水已由账户侧 settleBalance 按订单号写入,避免重复记账
            recordCardConsumeAllocate(order, actual, lockedAmount, afterBalance, code);
        }
        log.info("[SD结算] 账户结算完成 code={} accountMode={} locked={} actual={} balance={}", code,
                order.getAccountMode(), lockedAmount, actual, afterBalance);
        return true;
    }
 
    /**
     * 历史订单(改造前无锁定金额)扣款:沿用原有扣卡余额接口(订单号幂等)
     */
    private boolean legacyCardDeduct(TCardChargingOrder order, BigDecimal actual, String code) {
        if (actual.compareTo(BigDecimal.ZERO) <= 0 || order.getCardId() == null) {
            return true;
        }
        ChargingCardDeductDTO deductDTO = new ChargingCardDeductDTO();
        deductDTO.setCardId(order.getCardId());
        deductDTO.setOrderCode(code);
        deductDTO.setAmount(actual);
        R<ChargingCardDeductVO> deductResult;
        try {
            deductResult = chargingCardClient.deductForCharging(deductDTO);
        } catch (Exception ex) {
            log.error("[SD结算] 扣款调用异常 code={} cardId={} amount={}", code, order.getCardId(), actual, ex);
            return false;
        }
        if (deductResult == null || deductResult.getCode() != R.SUCCESS || deductResult.getData() == null) {
            log.error("[SD结算] 扣款失败 code={} cardId={} amount={} msg={}", code, order.getCardId(), actual,
                    deductResult == null ? "null" : deductResult.getMsg());
            return false;
        }
        if (deductResult.getData().isDuplicated()) {
            log.warn("[SD结算] 扣款幂等命中(重复请求不重复扣)code={}", code);
            return true;
        }
        recordCardConsumeAllocate(order, actual, BigDecimal.ZERO, deductResult.getData().getAfterBalance(), code);
        return true;
    }
 
    /**
     * 写企业额度分配记录-消费流水(仅卡侧扣款场景:分配模式结算与历史订单卡扣款,记该卡账户):
     * 共享模式消费流水由账户侧 settleBalance 按订单号写入(同一订单号+变更类型只能有一个写入方,避免重复记账);
     * 消费前余额 = 结算后余额 + 实际消费 - 锁定额度(实扣冻结口径反推);失败不影响结算主流程
     */
    private void recordCardConsumeAllocate(TCardChargingOrder order, BigDecimal actual, BigDecimal lockedAmount,
                                           BigDecimal afterBalance, String code) {
        if (order.getCompanyId() == null) {
            return;
        }
        BigDecimal after = afterBalance == null ? BigDecimal.ZERO : afterBalance;
        BigDecimal before = after.add(actual).subtract(lockedAmount);
        chargeAccountSupport.recordConsume(order.getCompanyId(), TCompanyAllocateRecord.USER_TYPE_CARD,
                order.getCardNo(), order.getCardNo(), before, actual.negate(), code, "刷卡充电结算");
    }
 
    private boolean rollbackFinishedToStopping(Long orderId, String code) {
        boolean rolledBack = lambdaUpdate()
                .eq(TCardChargingOrder::getId, orderId)
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
                .set(TCardChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .update();
        if (!rolledBack) {
            log.warn("[SD结算] 状态5回退4未命中,订单状态可能已变化 code={}", code);
        }
        return rolledBack;
    }
 
    /**
     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
     */
    private void pushRegulatoryAsync(TCardChargingOrder 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("[SD监管] 推送监管平台失败 code={}", order.getCode(), ex);
            }
        }, "SD-Regulatory-Push").start();
    }
 
    private boolean unlockCardQuietly(TCardChargingOrder order) {
        if (order.getCardId() == null) {
            return true;
        }
        try {
            R<Boolean> result = chargingCardClient.unlockForCharging(order.getCardId(), order.getCode());
            boolean success = result != null && result.getCode() == R.SUCCESS && Boolean.TRUE.equals(result.getData());
            if (!success) {
                log.error("[SD资源] 解锁卡片失败 code={} cardId={} msg={}", order.getCode(), order.getCardId(),
                        result == null ? "null" : result.getMsg());
            }
            return success;
        } catch (Exception ex) {
            log.error("[SD资源] 解锁卡片异常 code={} cardId={}", order.getCode(), order.getCardId(), ex);
            return false;
        }
    }
 
    private void resetGunQuietly(TCardChargingOrder order) {
        updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位");
    }
 
    private boolean updateGunStatusQuietly(TCardChargingOrder 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("[SD资源] {}查询充电枪失败 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("[SD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(),
                        order.getChargingGunId(), result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[SD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex);
            return false;
        }
    }
 
    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("[SD结算] 设备时间格式异常 value={}", value);
            return fallback;
        }
    }
 
    /**
     * 截取快照时段位 HH:mm(对齐 CD 充电 TaskUtil.endOrder 中 detailOrder.getStartTime().split(" ")[1]):
     * 1) snapshot 已为 HH:mm 直接返回;
     * 2) 若被上游拼接为 yyyy-MM-dd HH:mm:ss / yyyy-MM-dd HH:mm 等带日期格式,按空格 split 取末段。
     */
    private static String splitPeriodHourMinute(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty()) {
            return trimmed;
        }
        int spaceIdx = trimmed.lastIndexOf(' ');
        return spaceIdx < 0 ? trimmed : trimmed.substring(spaceIdx + 1);
    }
 
    // ===== 定时补偿(文档13.3:异常断电/补账场景,走统一结算入口) =====
 
    @Override
    public List<TCardChargingOrder> listTimeoutStartingOrders() {
        return lambdaQuery()
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                .lt(TCardChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(SD_STARTING_TIMEOUT_MINUTES))
                .list();
    }
 
    @Override
    public List<TCardChargingOrder> listTimeoutChargingOrders() {
        return lambdaQuery()
                .in(TCardChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .list();
    }
 
    @Override
    public int recoverFinishedOrdersWithoutDeduct() {
        List<TCardChargingOrder> candidates = lambdaQuery()
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_FINISHED)
                .isNotNull(TCardChargingOrder::getCardId)
                .gt(TCardChargingOrder::getOrderAmount, BigDecimal.ZERO)
                .lt(TCardChargingOrder::getEndTime, LocalDateTime.now().minusMinutes(SD_DEDUCT_COMPENSATION_DELAY_MINUTES))
                .orderByAsc(TCardChargingOrder::getEndTime)
                .last("LIMIT " + SD_DEDUCT_COMPENSATION_BATCH_SIZE)
                .list();
        if (candidates.isEmpty()) {
            return 0;
        }
 
        List<String> orderCodes = candidates.stream().map(TCardChargingOrder::getCode)
                .filter(StringUtils::isNotEmpty).collect(Collectors.toList());
        // 双判据:卡侧扣款流水(分配模式/历史卡扣款)与企业侧消费流水(共享模式),任一存在即视为已结算
        R<List<String>> deductedResult = chargingCardClient.listDeductedOrderCodes(orderCodes);
        if (deductedResult == null || deductedResult.getCode() != R.SUCCESS || deductedResult.getData() == null) {
            log.error("[SD扣款补偿] 查询卡侧扣款流水失败,本轮不修改订单 msg={}",
                    deductedResult == null ? "null" : deductedResult.getMsg());
            return 0;
        }
        java.util.Set<String> consumedCodes = chargeAccountSupport.listConsumedOrderCodesQuietly(orderCodes);
        if (consumedCodes == null) {
            log.error("[SD扣款补偿] 查询企业侧消费流水失败,本轮不修改订单");
            return 0;
        }
 
        java.util.Set<String> deductedCodes = new java.util.HashSet<>(deductedResult.getData());
        deductedCodes.addAll(consumedCodes);
        int recovered = 0;
        for (TCardChargingOrder candidate : candidates) {
            if (StringUtils.isEmpty(candidate.getCode()) || deductedCodes.contains(candidate.getCode())) {
                continue;
            }
            RLock settleLock = redissonClient.getLock(LOCK_SD_SETTLE + candidate.getCode());
            try {
                if (!settleLock.tryLock(0, 60, TimeUnit.SECONDS)) {
                    continue;
                }
                R<List<String>> recheck = chargingCardClient.listDeductedOrderCodes(
                        Collections.singletonList(candidate.getCode()));
                if (recheck == null || recheck.getCode() != R.SUCCESS || recheck.getData() == null) {
                    continue;
                }
                // 双判据复核:任一侧查询失败或存在流水即不回退(保守处理,避免重复扣款)
                java.util.Set<String> consumedRecheck = chargeAccountSupport.listConsumedOrderCodesQuietly(
                        Collections.singletonList(candidate.getCode()));
                if (consumedRecheck == null || recheck.getData().contains(candidate.getCode())
                        || consumedRecheck.contains(candidate.getCode())) {
                    continue;
                }
                if (rollbackFinishedToStopping(candidate.getId(), candidate.getCode())) {
                    recovered++;
                    log.warn("[SD扣款补偿] 已结束订单无扣款流水,状态回退停止中 code={}", candidate.getCode());
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                log.warn("[SD扣款补偿] 获取结算锁被中断 code={}", candidate.getCode());
                break;
            } catch (Exception ex) {
                log.error("[SD扣款补偿] 订单处理异常,保留原状态 code={}", candidate.getCode(), ex);
            } finally {
                if (settleLock.isHeldByCurrentThread()) {
                    settleLock.unlock();
                }
            }
        }
        return recovered;
    }
 
    @Override
    public void closeTimeoutStartingOrder(TCardChargingOrder order) {
        TCardChargingOrder update = new TCardChargingOrder();
        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<TCardChargingOrder>()
                .eq(TCardChargingOrder::getId, order.getId())
                .eq(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING));
        if (!closed) {
            log.info("[SD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode());
            return;
        }
        if (!unlockCardQuietly(order)) {
            // 解锁失败恢复为启动中,下轮继续补偿,避免卡片永久占用
            lambdaUpdate().eq(TCardChargingOrder::getId, order.getId())
                    .eq(TCardChargingOrder::getStatus, ORDER_STATUS_START_FAILED)
                    .set(TCardChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TCardChargingOrder::getEndMode, null)
                    .set(TCardChargingOrder::getEndTime, null)
                    .update();
            return;
        }
        // 释放申请时锁定的账户额度(未产生消费;失败仅告警,由人工/后续补偿核对)
        unlockCardChargeContextQuietly(order, "启动关单");
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[SD补偿] 启动超时关单 code={} cardId={} gunId={}", order.getCode(), order.getCardId(), order.getChargingGunId());
    }
 
    @Override
    public void compensationSettle(TCardChargingOrder 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("[SD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
                order.getCode(), flatPeriodIndex, capacity);
        cardEndChargeBillingCharge(vo);
    }
 
    private int resolveCompensationFlatPeriodIndex(Long orderId) {
        List<TCardAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(cardAccountingStrategyDetailOrderService
                        .lambdaQuery().eq(TCardAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
                .orElse(Collections.emptyList()).stream()
                .filter(item -> item.getType() != null)
                .sorted(Comparator.comparing(TCardAccountingStrategyDetailOrder::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("[SD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
        return 1;
    }
}