puzhibing
2026-09-15 262eb0096f28537c2859f79cdfa466092dc4f898
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
package com.ruoyi.order.service.impl;
 
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.feignClient.AppUserClient;
import com.ruoyi.account.api.feignClient.VinManagementClient;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.api.model.TVinManagement;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.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.model.Site;
import com.ruoyi.chargingPile.api.model.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.bean.BeanUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.integration.api.feignClient.ChargingMessageClient;
import com.ruoyi.integration.api.feignClient.ChengDuJianGuanClient;
import com.ruoyi.integration.api.feignClient.SendMessageClient;
import com.ruoyi.integration.api.model.PlatformStopCharging;
import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData;
import com.ruoyi.integration.api.vo.ChargingOrderVo;
import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder;
import com.ruoyi.order.api.model.TVinAccountingStrategyOrder;
import com.ruoyi.order.api.model.TVinChargingOrder;
import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
import com.ruoyi.order.api.query.VinChargingOrderQuery;
import com.ruoyi.order.api.vo.ChargingOrderVO;
import com.ruoyi.order.api.vo.DeviceStartChargingVO;
import com.ruoyi.order.api.vo.PlatformStartChargingReplyMessageVO;
import com.ruoyi.order.api.vo.PlatformStopChargingReplyVO;
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
import com.ruoyi.order.api.vo.VinChargingStatusVO;
import com.ruoyi.order.dto.MyVinChargingOrderList;
import com.ruoyi.order.mapper.TVinChargingOrderMapper;
import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TVinAccountingStrategyOrderService;
import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService;
import com.ruoyi.order.service.TVinChargingOrderService;
import com.ruoyi.order.util.TransactionStageParser;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
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.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
 
@Service
public class TVinChargingOrderServiceImpl extends ServiceImpl<TVinChargingOrderMapper, TVinChargingOrder>
        implements TVinChargingOrderService {
 
    private static final Logger log = LoggerFactory.getLogger(TVinChargingOrderServiceImpl.class);
 
    /** VD 订单号前缀(设备VIN码充电) */
    private static final String ORDER_CODE_PREFIX_VD = "VD";
    /** 订单状态:启动中 */
    private static final int ORDER_STATUS_STARTING = 2;
    /** 订单状态:充电中 */
    private static final int ORDER_STATUS_CHARGING = 3;
    /** 订单状态:停止中 */
    private static final int ORDER_STATUS_STOPPING = 4;
    /** 订单状态:已结束 */
    private static final int ORDER_STATUS_FINISHED = 5;
    /** 订单状态:启动失败(沿用 t_charging_order 枚举约定:-1 为负值语义,前端按 status<0 渲染) */
    private static final int ORDER_STATUS_START_FAILED = -1;
    /** 充电枪状态:充电中 */
    private static final int GUN_STATUS_CHARGING = 4;
    /** 充电枪状态:空闲 */
    private static final int GUN_STATUS_IDLE = 2;
    /** VD 建单枪锁前缀(桩+枪维度,防同枪并发建单,文档13.2) */
    private static final String LOCK_VD_START_GUN = "VD_START_GUN:";
    /** VD 建单VIN锁前缀(VIN ID维度,防同VIN多枪并发启动,文档13.2) */
    private static final String LOCK_VD_START_VIN = "VD_START_VIN:";
    /** VD 结算锁前缀(订单号维度,防并发重复结算,文档13.3) */
    private static final String LOCK_VD_SETTLE = "VD_SETTLE:";
    /** VD 启动中超时阈值(分钟):超时未收到启动成功应答则关单 */
    private static final int VD_STARTING_TIMEOUT_MINUTES = 30;
    /** VD 充电中超时阈值(小时):超时未收到交易记录则兜底结算 */
    private static final int VD_CHARGING_TIMEOUT_HOURS = 24;
    /** 列表/详情回填 VIN 信息时,单次批量远程查询的最大 ids 数量 */
    private static final int VIN_BACKFILL_BATCH_SIZE = 500;
 
    @Resource
    private TokenService tokenService;
    @Resource
    private ChargingMessageClient chargingMessageClient;
    @Resource
    private ChengDuJianGuanClient chengDuJianGuanClient;
    @Resource
    private VinManagementClient vinManagementClient;
    @Resource
    private AppUserClient appUserClient;
    @Resource
    private SiteClient siteClient;
    @Resource
    private ChargingGunClient chargingGunClient;
    @Resource
    private ChargingPileClient chargingPileClient;
    @Resource
    private SendMessageClient sendMessageClient;
    @Resource
    private UploadRealTimeMonitoringDataService monitoringDataService;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
    private AccountingStrategyDetailClient accountingStrategyDetailClient;
    @Resource
    private TVinAccountingStrategyOrderService vinAccountingStrategyOrderService;
    @Resource
    private TVinAccountingStrategyDetailOrderService vinAccountingStrategyDetailOrderService;
    @Resource
    private TVinChargingOrderAccountingStrategyService vinChargingOrderAccountingStrategyService;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private RedissonClient redissonClient;
 
    @Override
    public PageInfo<ChargingOrderVO> pageList(VinChargingOrderQuery query) {
        int pageCurr = query.getPageCurr() == null || query.getPageCurr() < 1 ? 1 : query.getPageCurr();
        int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 50 : query.getPageSize();
        setAuthSiteIds(query);
        PageInfo<ChargingOrderVO> page = new PageInfo<>(pageCurr, pageSize);
        long total = baseMapper.count(query);
        page.setTotal(total);
        List<ChargingOrderVO> records = total == 0 ? Collections.emptyList()
                : baseMapper.pageList(query, (long) (pageCurr - 1) * pageSize, pageSize);
        fillExtra(records);
        page.setRecords(records);
        return page;
    }
 
    @Override
    public PageInfo<MyVinChargingOrderList> pageAppletOrders(Integer pageCurr, Integer pageSize) {
        int current = pageCurr == null || pageCurr < 1 ? 1 : pageCurr;
        int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
        PageInfo<MyVinChargingOrderList> page = new PageInfo<>(current, size);
        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        Long appUserId = tokenService.getLoginUserApplet().getUserId();
        R<List<Long>> vinIdResponse = vinManagementClient.getVinIdsByAppUserId(appUserId);
        List<Long> vinIds = vinIdResponse == null ? Collections.emptyList()
                : Optional.ofNullable(vinIdResponse.getData()).orElse(Collections.emptyList());
        if (vinIds.isEmpty()) {
            page.setTotal(0);
            page.setRecords(Collections.emptyList());
            return page;
        }
        long total = baseMapper.countAppletOrders(vinIds);
        page.setTotal(total);
        List<MyVinChargingOrderList> records = total == 0 ? Collections.emptyList()
                : baseMapper.pageAppletOrders(vinIds, (long) (current - 1) * size, size);
        fillAppletExtra(records);
        page.setRecords(records);
        return page;
    }
 
    @Override
    public Map<String, Object> appletDetail(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedAppletOrder(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", String.valueOf(order.getId()));
        result.put("chargingGunId", order.getChargingGunId());
        result.put("code", order.getCode());
        result.put("createTime", order.getCreateTime() == null ? null
                : order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("status", order.getStatus());
        result.put("licensePlate", order.getPlateNum());
        result.put("startTime", toEpochMilli(order.getStartTime()));
        result.put("endTime", toEpochMilli(order.getEndTime()));
        result.put("orderAmount", order.getOrderAmount());
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("rechargeAmount", order.getRechargeAmount());
        result.put("couponDiscountAmount", order.getCouponDiscountAmount());
        result.put("vipDiscountAmount", order.getVipDiscountAmount());
        result.put("refundAmount", order.getRefundAmount());
        result.put("electricMoney", order.getElectrovalence());
        result.put("serviceMoney", order.getServiceCharge());
        result.put("totalElectricQuantity", order.getChargingCapacity());
        result.put("stageCost", baseMapper.accountingStrategyList(order.getId()));
        result.put("actionable", 0);
        result.put("isAppeal", 0);
 
        Site site = getSite(order.getSiteId());
        result.put("title", site == null ? "未知电站" : site.getName());
        TChargingGun gun = getGun(order.getChargingGunId());
        result.put("chargeMode", gun == null ? null : gun.getChargeMode());
        result.put("name", gun == null ? null : gun.getName());
        fillAppletMonitoring(result, order.getCode());
        return result;
    }
 
    @Override
    public List<VinChargingStatusVO> getChargingVinStatus(List<Long> vinIds) {
        if (vinIds == null || vinIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> distinctVinIds = vinIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        return distinctVinIds.isEmpty() ? Collections.emptyList() : baseMapper.getChargingVinStatus(distinctVinIds);
    }
 
    @Override
    public List<ChargingOrderVO> exportList(VinChargingOrderQuery query) {
        setAuthSiteIds(query);
        long total = baseMapper.count(query);
        List<ChargingOrderVO> result = new ArrayList<>();
        for (long offset = 0; offset < total; offset += 5000) {
            List<ChargingOrderVO> batch = baseMapper.pageList(query, offset, 5000);
            if (batch == null || batch.isEmpty()) {
                break;
            }
            result.addAll(batch);
        }
        fillExtra(result);
        fillRealtimeData(result);
        return result;
    }
 
    @Override
    public Map<String, Object> summary(VinChargingOrderQuery query) {
        setAuthSiteIds(query);
        Map<String, Object> result = baseMapper.summary(query);
        return result == null ? new HashMap<>() : result;
    }
 
    @Override
    public Map<String, Object> detail(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
        if (order == null) {
            return Collections.emptyMap();
        }
        Map<String, Object> result = new HashMap<>();
        result.put("id", order.getId() == null ? null : String.valueOf(order.getId()));
        result.put("code", order.getCode());
        // 订单VIN快照为空时回填 VIN 管理登记值(历史订单无快照),前端按 actualVin → expectedVin → vin 取值
        TVinManagement vinRecord = order.getVinId() == null ? null
                : loadVinRecords(Collections.singletonList(order.getVinId())).get(order.getVinId());
        String vinCode = StringUtils.isEmpty(order.getActualVin()) ? order.getExpectedVin() : order.getActualVin();
        if (StringUtils.isEmpty(vinCode) && vinRecord != null) {
            vinCode = vinRecord.getVin();
        }
        result.put("vin", vinCode);
        result.put("actualVin", order.getActualVin());
        result.put("expectedVin", order.getExpectedVin());
        result.put("plateNum", StringUtils.isEmpty(order.getPlateNum()) && vinRecord != null
                ? vinRecord.getPlateNumber() : order.getPlateNum());
        result.put("phone", resolvePhone(order.getPhone(), order.getAppUserId(), vinRecord));
        result.put("status", order.getStatus());
        result.put("siteId", order.getSiteId());
        result.put("chargingPileId", order.getChargingPileId());
        result.put("chargingGunId", order.getChargingGunId());
        result.put("startTime", order.getStartTime());
        result.put("endTime", order.getEndTime());
        result.put("chargingCapacity", order.getChargingCapacity());
        result.put("paymentAmount", order.getPaymentAmount());
        result.put("electrovalence", order.getElectrovalence());
        result.put("serviceCharge", order.getServiceCharge());
        if (order.getStartTime() != null && order.getEndTime() != null) {
            result.put("chargingSecond", ChronoUnit.SECONDS.between(order.getStartTime(), order.getEndTime()));
        }
        List<Site> sites = order.getSiteId() == null ? Collections.emptyList()
                : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(order.getSiteId())).getData()).orElse(Collections.emptyList());
        Site site = sites.stream().findFirst().orElse(null);
        result.put("siteName", site == null ? null : site.getName());
        List<TChargingGun> guns = order.getChargingGunId() == null ? Collections.emptyList()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(Collections.singletonList(order.getChargingGunId())).getData()).orElse(Collections.emptyList());
        TChargingGun gun = guns.stream().findFirst().orElse(null);
        result.put("terminalName", gun == null ? null : gun.getName());
        List<UploadRealTimeMonitoringData> monitoring = monitoringDataService.getDataByOrderCodeAsc(order.getCode());
        if (monitoring == null) {
            monitoring = Collections.emptyList();
        }
        for (UploadRealTimeMonitoringData data : monitoring) {
            if (data.getOutput_current() != null && data.getOutput_voltage() != null) {
                data.setPower(data.getOutput_voltage().multiply(data.getOutput_current()).divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP));
            }
        }
        UploadRealTimeMonitoringData latestMonitoring = monitoring.isEmpty() ? null : monitoring.get(monitoring.size() - 1);
        result.put("cdElectronic", latestMonitoring == null ? null : latestMonitoring.getOutput_current());
        result.put("cdVoltage", latestMonitoring == null ? null : latestMonitoring.getOutput_voltage());
        result.put("totalPower", latestMonitoring == null ? null : latestMonitoring.getPower());
        result.put("surplus", latestMonitoring == null ? null : latestMonitoring.getSoc());
        result.put("list", monitoring);
        result.put("list1", baseMapper.accountingStrategyList(order.getId()));
        return result;
    }
 
    @Override
    public AjaxResult stopCharging(String chargingOrderId, Integer chargingGunId) {
        Long orderId;
        try {
            orderId = Long.valueOf(chargingOrderId);
        } catch (NumberFormatException e) {
            return AjaxResult.error("VIN码充电订单ID无效");
        }
        TVinChargingOrder order = getAuthorizedOrder(orderId);
        if (order == null) {
            return AjaxResult.error("VIN码充电订单不存在或无权操作");
        }
        if (order.getChargingGunId() == null || !order.getChargingGunId().equals(chargingGunId)) {
            return AjaxResult.error("充电枪与订单不匹配");
        }
        Integer status = order.getStatus();
        if (status == null || !status.equals(3)) {
            return AjaxResult.error(status != null && (status.equals(4) || status.equals(5)) ? "不能重复操作" : "还未开始充电");
        }
        TChargingGun chargingGun = chargingGunClient.getChargingGunById(chargingGunId).getData();
        TChargingPile chargingPile = order.getChargingPileId() == null ? null
                : chargingPileClient.getChargingPileById(order.getChargingPileId()).getData();
        if (chargingGun == null || chargingPile == null) {
            return AjaxResult.error("未找到订单对应的充电设备");
        }
        order.setStatus(4);
        order.setEndMode(1);
        order.setEndTime(LocalDateTime.now());
        if (!updateById(order)) {
            return AjaxResult.error("更新VIN码充电订单状态失败");
        }
        PlatformStopCharging command = new PlatformStopCharging();
        command.setCharging_pile_code(chargingPile.getCode());
        command.setCharging_gun_code(chargingGun.getCode());
        sendMessageClient.platformStopCharging(command);
        return AjaxResult.success();
    }
 
    @Override
    public List<Map<String, Object>> accountingStrategyList(Long chargingOrderId) {
        TVinChargingOrder order = getAuthorizedOrder(chargingOrderId);
        return order == null ? Collections.emptyList() : baseMapper.accountingStrategyList(chargingOrderId);
    }
 
    private void setAuthSiteIds(VinChargingOrderQuery query) {
        List<Integer> authorizedSiteIds = getAuthorizedSiteIds();
        if (query.getSiteId() != null) {
            authorizedSiteIds = authorizedSiteIds.contains(query.getSiteId())
                    ? Collections.singletonList(query.getSiteId()) : Collections.singletonList(-1);
        }
        if (authorizedSiteIds.isEmpty()) {
            authorizedSiteIds = Collections.singletonList(-1);
        }
        query.setSiteIds(authorizedSiteIds);
    }
 
    private TVinChargingOrder getAuthorizedOrder(Long chargingOrderId) {
        if (chargingOrderId == null) {
            return null;
        }
        TVinChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getSiteId() == null || !getAuthorizedSiteIds().contains(order.getSiteId())) {
            return null;
        }
        return order;
    }
 
    private List<Integer> getAuthorizedSiteIds() {
        Long userId = tokenService.getLoginUser().getUserid();
        if (userId == null) {
            return Collections.emptyList();
        }
        return Optional.ofNullable(siteClient.getSiteListByUserId(userId).getData())
                .orElse(Collections.emptyList()).stream().map(GetSiteListDTO::getId).collect(Collectors.toList());
    }
 
    private TVinChargingOrder getAuthorizedAppletOrder(Long chargingOrderId) {
        if (chargingOrderId == null || tokenService.getLoginUserApplet() == null
                || tokenService.getLoginUserApplet().getUserId() == null) {
            return null;
        }
        TVinChargingOrder order = getById(chargingOrderId);
        if (order == null || order.getVinId() == null) {
            return null;
        }
        R<List<Long>> response = vinManagementClient.getVinIdsByAppUserId(
                tokenService.getLoginUserApplet().getUserId());
        List<Long> vinIds = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return vinIds.contains(order.getVinId()) ? order : null;
    }
 
    private Long toEpochMilli(LocalDateTime time) {
        return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }
 
    private Site getSite(Integer siteId) {
        if (siteId == null) {
            return null;
        }
        R<List<Site>> response = siteClient.getSiteByIds(Collections.singletonList(siteId));
        List<Site> sites = response == null ? Collections.emptyList()
                : Optional.ofNullable(response.getData()).orElse(Collections.emptyList());
        return sites.stream().findFirst().orElse(null);
    }
 
    private TChargingGun getGun(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        R<TChargingGun> response = chargingGunClient.getChargingGunById(gunId);
        return response == null ? null : response.getData();
    }
 
    private void fillAppletMonitoring(Map<String, Object> result, String orderCode) {
        List<UploadRealTimeMonitoringData> dataList = orderCode == null ? Collections.emptyList()
                : monitoringDataService.getDataByOrderCode(orderCode);
        if (dataList == null || dataList.isEmpty()) {
            result.put("currentAndVoltage", Collections.emptyList());
            result.put("power", Collections.emptyList());
            return;
        }
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
        Map<String, UploadRealTimeMonitoringData> minuteData = new java.util.LinkedHashMap<>();
        for (UploadRealTimeMonitoringData data : dataList) {
            if (data != null && data.getCreate_time() != null) {
                minuteData.putIfAbsent(formatter.format(data.getCreate_time()), data);
            }
        }
        List<Map<String, Object>> currentAndVoltage = new ArrayList<>();
        List<Map<String, Object>> power = new ArrayList<>();
        for (Map.Entry<String, UploadRealTimeMonitoringData> entry : minuteData.entrySet()) {
            UploadRealTimeMonitoringData data = entry.getValue();
            Map<String, Object> currentItem = new HashMap<>();
            currentItem.put("time", entry.getKey());
            currentItem.put("electricity", data.getOutput_current());
            currentItem.put("voltage", data.getOutput_voltage());
            currentAndVoltage.add(currentItem);
 
            Map<String, Object> powerItem = new HashMap<>();
            powerItem.put("time", entry.getKey());
            powerItem.put("soc", data.getSoc());
            BigDecimal outputPower = data.getOutput_current() == null || data.getOutput_voltage() == null ? null
                    : data.getOutput_current().multiply(data.getOutput_voltage())
                    .divide(new BigDecimal("1000"), 2, RoundingMode.HALF_DOWN);
            powerItem.put("power", outputPower);
            power.add(powerItem);
        }
        result.put("currentAndVoltage", currentAndVoltage);
        result.put("power", power);
    }
 
    private void fillAppletExtra(List<MyVinChargingOrderList> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> siteIds = list.stream().map(MyVinChargingOrderList::getSiteId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(MyVinChargingOrderList::getChargingGunId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Integer, Site> sites = siteIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        Map<Integer, TChargingGun> guns = gunIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(gunIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(TChargingGun::getId, item -> item, (a, b) -> a));
        for (MyVinChargingOrderList item : list) {
            Site site = sites.get(item.getSiteId());
            item.setTitle(site == null ? "未知电站" : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setName(gun == null ? null : gun.getName());
        }
    }
 
    private void fillExtra(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> orderIds = list.stream().map(ChargingOrderVO::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, Integer> periodCounts = new HashMap<>();
        if (!orderIds.isEmpty()) {
            for (Map<String, Object> item : baseMapper.countAccountingStrategies(orderIds)) {
                Number chargingOrderId = (Number) item.get("chargingOrderId");
                Number periodCount = (Number) item.get("periodCount");
                if (chargingOrderId != null && periodCount != null) {
                    periodCounts.put(chargingOrderId.longValue(), periodCount.intValue());
                }
            }
        }
        List<Integer> siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(ChargingOrderVO::getChargingGunId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Integer, Site> sites = siteIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(siteClient.getSiteByIds(siteIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(Site::getId, item -> item, (a, b) -> a));
        Map<Integer, TChargingGun> guns = gunIds.isEmpty() ? Collections.emptyMap()
                : Optional.ofNullable(chargingGunClient.getChargingGunByIds(gunIds).getData()).orElse(Collections.emptyList()).stream()
                .collect(Collectors.toMap(TChargingGun::getId, item -> item, (a, b) -> a));
        for (ChargingOrderVO item : list) {
            Site site = sites.get(item.getSiteId());
            item.setSiteName(site == null ? null : site.getName());
            TChargingGun gun = guns.get(item.getChargingGunId());
            item.setTerminalName(gun == null ? null : gun.getName());
            if (item.getStartTime() != null && item.getEndTime() != null) {
                item.setChargingSecond(ChronoUnit.SECONDS.between(item.getStartTime(), item.getEndTime()));
            }
            item.setCount(periodCounts.getOrDefault(item.getId(), 0));
            item.setPlatformIncome(BigDecimal.ZERO);
            item.setPartnerIncome(BigDecimal.ZERO);
        }
        fillVinInfoAndPhone(list);
    }
 
    /**
     * 回填 VIN 码/车牌号/手机号:订单快照为空时取 VIN 管理登记值(历史订单无快照),
     * 手机号再以 App 用户当前手机号兜底。远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillVinInfoAndPhone(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> vinIds = list.stream().map(ChargingOrderVO::getVinId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, TVinManagement> vinRecords = loadVinRecords(vinIds);
        for (ChargingOrderVO item : list) {
            TVinManagement vinRecord = item.getVinId() == null ? null : vinRecords.get(item.getVinId());
            if (vinRecord == null) {
                continue;
            }
            if (StringUtils.isEmpty(item.getVin())) {
                item.setVin(vinRecord.getVin());
            }
            if (StringUtils.isEmpty(item.getPlateNum())) {
                item.setPlateNum(vinRecord.getPlateNumber());
            }
            if (StringUtils.isEmpty(item.getPhone())) {
                item.setPhone(vinRecord.getPhone());
            }
        }
        List<Long> appUserIds = list.stream()
                .filter(item -> StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null)
                .map(ChargingOrderVO::getAppUserId).distinct().collect(Collectors.toList());
        if (appUserIds.isEmpty()) {
            return;
        }
        Map<Long, String> userPhones = loadAppUserPhones(appUserIds);
        for (ChargingOrderVO item : list) {
            if (StringUtils.isEmpty(item.getPhone()) && item.getAppUserId() != null) {
                String phone = userPhones.get(item.getAppUserId());
                if (StringUtils.isNotEmpty(phone)) {
                    item.setPhone(phone);
                }
            }
        }
    }
 
    /** 手机号回填优先级:订单值 → VIN 记录手机号 → App 用户当前手机号 */
    private String resolvePhone(String phone, Long appUserId, TVinManagement vinRecord) {
        if (StringUtils.isNotEmpty(phone)) {
            return phone;
        }
        if (vinRecord != null && StringUtils.isNotEmpty(vinRecord.getPhone())) {
            return vinRecord.getPhone();
        }
        if (appUserId == null) {
            return null;
        }
        return loadAppUserPhones(Collections.singletonList(appUserId)).get(appUserId);
    }
 
    /** 按 VIN 记录ID批量查询,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */
    private Map<Long, TVinManagement> loadVinRecords(List<Long> vinIds) {
        Map<Long, TVinManagement> result = new HashMap<>();
        if (vinIds == null || vinIds.isEmpty()) {
            return result;
        }
        for (int index = 0; index < vinIds.size(); index += VIN_BACKFILL_BATCH_SIZE) {
            List<Long> batch = vinIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, vinIds.size()));
            try {
                R<List<TVinManagement>> response = vinManagementClient.getVinByIds(new ArrayList<>(batch));
                if (response == null || response.getData() == null) {
                    continue;
                }
                for (TVinManagement record : response.getData()) {
                    if (record != null && record.getId() != null) {
                        result.putIfAbsent(record.getId(), record);
                    }
                }
            } catch (Exception e) {
                log.warn("[VD回填] 批量查询VIN记录失败 ids={}", batch, e);
            }
        }
        return result;
    }
 
    /** 按 App 用户ID批量查询当前手机号,每批 {@link #VIN_BACKFILL_BATCH_SIZE} 个,失败返回已取到的数据 */
    private Map<Long, String> loadAppUserPhones(List<Long> appUserIds) {
        Map<Long, String> result = new HashMap<>();
        for (int index = 0; index < appUserIds.size(); index += VIN_BACKFILL_BATCH_SIZE) {
            List<Long> batch = appUserIds.subList(index, Math.min(index + VIN_BACKFILL_BATCH_SIZE, appUserIds.size()));
            try {
                R<List<TAppUser>> response = appUserClient.getUserByIds(new ArrayList<>(batch));
                if (response == null || response.getData() == null) {
                    continue;
                }
                for (TAppUser user : response.getData()) {
                    if (user != null && user.getId() != null && StringUtils.isNotEmpty(user.getPhone())) {
                        result.putIfAbsent(user.getId(), user.getPhone());
                    }
                }
            } catch (Exception e) {
                log.warn("[VD回填] 批量查询App用户手机号失败 ids={}", batch, e);
            }
        }
        return result;
    }
 
    private void fillRealtimeData(List<ChargingOrderVO> list) {
        for (ChargingOrderVO item : list) {
            if (item.getCode() == null || item.getCode().trim().isEmpty()) {
                continue;
            }
            UploadRealTimeMonitoringData data = monitoringDataService.getLastDataById(item.getCode());
            if (data == null) {
                continue;
            }
            item.setChargeCurrent(data.getOutput_current());
            item.setChargeVoltage(data.getOutput_voltage());
            BigDecimal power = data.getPower();
            if (power == null && data.getOutput_current() != null && data.getOutput_voltage() != null) {
                power = data.getOutput_current().multiply(data.getOutput_voltage())
                        .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            }
            item.setChargePower(power);
        }
    }
 
    // ===== 设备侧VIN码充电链路(V0.2 VIN码充电) =====
 
    @Override
    public DeviceStartChargingVO startVinCharging(DeviceStartChargingDTO dto) {
        if (dto == null || StringUtils.isEmpty(dto.getChargingPileCode())
                || StringUtils.isEmpty(dto.getChargingGunCode()) || StringUtils.isEmpty(dto.getAccount())) {
            return DeviceStartChargingVO.fail(9);
        }
        // 并发防护(文档13.2):桩+枪维度分布式锁,抢不到说明同枪并发启动,快速失败
        RLock gunLock = redissonClient.getLock(LOCK_VD_START_GUN + dto.getChargingPileCode() + ":" + dto.getChargingGunCode());
        try {
            if (!gunLock.tryLock(0, 30, TimeUnit.SECONDS)) {
                return DeviceStartChargingVO.fail(10);
            }
            // 1. 充电枪校验
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(dto.getChargingPileCode());
            gunQuery.setCharging_gun_code(dto.getChargingGunCode());
            TChargingGun gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
            if (gun == null) {
                return DeviceStartChargingVO.fail(5);
            }
            if (gun.getStatus() != null && gun.getStatus() == GUN_STATUS_CHARGING) {
                return DeviceStartChargingVO.fail(10);
            }
            TChargingPile pile = gun.getChargingPileId() == null ? null
                    : chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData();
            Site site = gun.getSiteId() == null ? null
                    : Optional.ofNullable(siteClient.getSiteByIds(Collections.singletonList(gun.getSiteId())).getData())
                            .orElse(Collections.emptyList()).stream().findFirst().orElse(null);
 
            // 2. VIN反序解析与绑定校验(协议要求VIN码反序上送)
            String vin = new StringBuilder(dto.getAccount().trim()).reverse().toString();
            TVinManagement vinRecord = vinManagementClient.getVinByCode(vin).getData();
            if (vinRecord == null) {
                return DeviceStartChargingVO.fail(9);
            }
            if (vinRecord.getStatus() == null || vinRecord.getStatus() != 1) {
                return DeviceStartChargingVO.fail(2);
            }
 
            // 并发防护(文档13.2):VIN ID维度锁,防同VIN多枪并发建单
            RLock vinLock = redissonClient.getLock(LOCK_VD_START_VIN + vinRecord.getId());
            boolean vinLocked = false;
            try {
                vinLocked = vinLock.tryLock(0, 30, TimeUnit.SECONDS);
                if (!vinLocked) {
                    return DeviceStartChargingVO.fail(10);
                }
                // 3. 建单(状态2 启动中)
                TVinChargingOrder order = new TVinChargingOrder();
                order.setCode(ORDER_CODE_PREFIX_VD + Double.valueOf(Math.random() * 1000).intValue() + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
                order.setOrderType(2);
                order.setOrderSource(1);
                order.setOrderClassification(2);
                // VD 无支付动作(后付记账模式,不执行在线支付/扣款):payMethod 留空,
                // 不套用 CD 余额支付枚举,避免下游按支付模式误判走余额扣款逻辑
                order.setStatus(ORDER_STATUS_STARTING);
                order.setTitle("【充电桩充电】" + (pile != null && pile.getNumber() != null ? pile.getNumber() : dto.getChargingPileCode())
                        + "号桩/" + dto.getChargingGunCode() + "号枪");
                order.setSiteId(gun.getSiteId());
                order.setChargingPileId(gun.getChargingPileId());
                order.setChargingGunId(gun.getId());
                order.setVinId(vinRecord.getId());
                order.setAppUserId(vinRecord.getAppUserId());
                // VIN 码/车牌快照落库(协议上报为反序VIN,此处存解析后的正序VIN),保证列表与详情直接可见
                order.setExpectedVin(vinRecord.getVin());
                order.setActualVin(vin);
                order.setPlateNum(vinRecord.getPlateNumber());
                order.setCreateTime(LocalDateTime.now());
                save(order);
 
                // 4. 计费策略快照(枪策略优先,站点策略兜底)
                Integer strategyId = gun.getAccountingStrategyId() != null
                        ? gun.getAccountingStrategyId()
                        : (site == null ? null : site.getAccountingStrategyId());
                try {
                    saveStrategySnapshot(order.getId(), strategyId);
                } catch (Exception snapshotEx) {
                    log.error("[VD建单] 保存计费策略快照失败 orderId={} strategyId={}", order.getId(), strategyId, snapshotEx);
                }
                log.info("[VD建单] VIN码充电订单创建成功 code={} vinId={} gun={}", order.getCode(), vinRecord.getId(), dto.getChargingGunCode());
                return DeviceStartChargingVO.success(order.getCode(), null);
            } finally {
                if (vinLocked && vinLock.isHeldByCurrentThread()) {
                    vinLock.unlock();
                }
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            log.error("[VD建单] 建单锁获取被中断 pile={} gun={}", dto.getChargingPileCode(), dto.getChargingGunCode(), ie);
            return DeviceStartChargingVO.fail(null);
        } catch (Exception e) {
            log.error("[VD建单] VIN码充电启动异常 pile={} gun={} account={}",
                    dto.getChargingPileCode(), dto.getChargingGunCode(), dto.getAccount(), e);
            return DeviceStartChargingVO.fail(null);
        } finally {
            if (gunLock.isHeldByCurrentThread()) {
                gunLock.unlock();
            }
        }
    }
 
    /**
     * 保存VIN订单计费策略快照(主表 + 明细表)
     */
    private void saveStrategySnapshot(Long orderId, Integer strategyId) {
        if (strategyId == null) {
            return;
        }
        TAccountingStrategy strategy = accountingStrategyClient.getAccountingStrategyById(strategyId).getData();
        List<TAccountingStrategyDetail> details = accountingStrategyDetailClient.getListByAccountingStrategyId(strategyId).getData();
        if (strategy == null || details == null || details.isEmpty()) {
            return;
        }
        TVinAccountingStrategyOrder strategyOrder = new TVinAccountingStrategyOrder();
        BeanUtils.copyProperties(strategy, strategyOrder);
        strategyOrder.setChargingOrderId(orderId);
        vinAccountingStrategyOrderService.save(strategyOrder);
        List<TVinAccountingStrategyDetailOrder> detailOrders = new ArrayList<>();
        for (TAccountingStrategyDetail detail : details) {
            TVinAccountingStrategyDetailOrder detailOrder = new TVinAccountingStrategyDetailOrder();
            BeanUtils.copyProperties(detail, detailOrder);
            detailOrder.setChargingOrderId(orderId);
            detailOrders.add(detailOrder);
        }
        vinAccountingStrategyDetailOrderService.saveBatch(detailOrders);
    }
 
    @Override
    public TVinChargingOrder getVinOrderByCode(String code) {
        if (StringUtils.isEmpty(code)) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getCode, code)
                .last("limit 1"));
    }
 
    @Override
    public void vinStartChargeSuccessfully(PlatformStartChargingReplyMessageVO vo) {
        if (vo == null || StringUtils.isEmpty(vo.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(vo.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只有启动中订单可消费启动应答,防止迟到消息复活已关闭/已结束订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())) {
            return;
        }
        if (vo.getStartup_result() != null && vo.getStartup_result() == 1) {
            boolean updated = lambdaUpdate()
                    .eq(TVinChargingOrder::getId, order.getId())
                    .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
                    .set(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING)
                    .set(TVinChargingOrder::getStartTime, LocalDateTime.now())
                    .update();
            if (!updated) {
                return;
            }
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "启动成功");
            log.info("[VD启动] VIN订单进入充电中 code={}", order.getCode());
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
        } else {
            closeTimeoutStartingOrder(order);
            log.warn("[VD启动] VIN订单启动失败关闭 code={} result={}", order.getCode(), vo.getStartup_result());
        }
    }
 
    @Override
    public boolean handleHandshakeVin(String orderCode, String vin) {
        if (StringUtils.isEmpty(orderCode)) {
            return false;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            log.warn("[VD握手] 订单不存在 code={}", orderCode);
            return false;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        boolean needUpdate = false;
        if (StringUtils.isNotEmpty(vin)) {
            update.setActualVin(vin);
            needUpdate = true;
        }
        boolean advanced = order.getStatus() != null && order.getStatus() == ORDER_STATUS_STARTING;
        if (advanced) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
            needUpdate = true;
        }
        if (!needUpdate) {
            return true;
        }
        boolean updated;
        if (advanced) {
            updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                    .eq(TVinChargingOrder::getId, order.getId())
                    .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
        } else {
            updated = updateById(update);
        }
        if (advanced && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "握手恢复");
            order.setStatus(ORDER_STATUS_CHARGING);
            pushRegulatoryAsync(order, true, false);
            log.info("[VD握手] 握手推进订单进入充电中 code={}", orderCode);
        }
        return updated;
    }
 
    @Override
    public void vinChargeMonitoring(UploadRealTimeMonitoringDataQuery query) {
        if (query == null || StringUtils.isEmpty(query.getTransaction_serial_number())) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(query.getTransaction_serial_number());
        if (order == null) {
            return;
        }
        // 只接收启动中/充电中的监控数据,禁止迟到数据复活终态订单
        if (!Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus())
                && !Integer.valueOf(ORDER_STATUS_CHARGING).equals(order.getStatus())) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        boolean starting = Integer.valueOf(ORDER_STATUS_STARTING).equals(order.getStatus());
        if (starting) {
            update.setStatus(ORDER_STATUS_CHARGING);
            if (order.getStartTime() == null) {
                update.setStartTime(LocalDateTime.now());
            }
        }
        if (query.getOutput_current() != null && query.getOutput_current().compareTo(BigDecimal.ZERO) != 0
                && query.getOutput_voltage() != null) {
            BigDecimal power = query.getOutput_current().multiply(query.getOutput_voltage())
                    .divide(new BigDecimal(1000), 2, BigDecimal.ROUND_HALF_UP);
            update.setChargingPower(power);
            update.setCurrent(query.getOutput_current());
            update.setVoltage(query.getOutput_voltage());
            update.setPower(power);
            update.setNeedElec(query.getOutput_current());
            TChargingGun gun = new TChargingGun();
            gun.setId(order.getChargingGunId());
            gun.setChargingPower(power);
            chargingGunClient.updateChargingGunById(gun);
        }
        if (query.getCharging_degree() != null) {
            update.setChargingCapacity(query.getCharging_degree());
            update.setElectricity(query.getCharging_degree());
        }
        boolean updated = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getId, order.getId())
                .in(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING, ORDER_STATUS_CHARGING));
        if (starting && updated) {
            updateGunStatusQuietly(order, GUN_STATUS_CHARGING, "实时数据恢复");
        }
    }
 
    @Override
    public void vinEndCharge(String orderCode, Integer endMode) {
        if (StringUtils.isEmpty(orderCode)) {
            return;
        }
        TVinChargingOrder order = getVinOrderByCode(orderCode);
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(endMode);
        updateById(update);
        pushRegulatoryAsync(order, false, false);
    }
 
    @Override
    public R vinEndChargeBillingCharge(TransactionRecordMessageVO vo) {
        String code = vo == null ? null : vo.getTransaction_serial_number();
        log.info("[VD结算] 收到交易记录 code={}", code);
        if (StringUtils.isEmpty(code)) {
            return R.fail("交易记录缺少交易流水号");
        }
        // 结算锁(文档13.3 统一结算入口第一层):订单号维度,防并发/重复账单同时进入结算;
        // 抢不到锁说明正在结算,按重复账单语义返回成功;锁内仍有状态幂等第二层兜底
        RLock settleLock = redissonClient.getLock(LOCK_VD_SETTLE + code);
        try {
            if (!settleLock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.warn("[VD结算] 并发结算进行中,跳过重复账单 code={}", code);
                return R.ok("重复账单,结算处理中");
            }
            return doVinEndChargeBillingCharge(vo, code);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return R.fail("结算锁获取被中断:" + code);
        } finally {
            if (settleLock.isHeldByCurrentThread()) {
                settleLock.unlock();
            }
        }
    }
 
    /**
     * VD 结算实际执行体(调用前须持有订单号结算锁)
     */
    private R doVinEndChargeBillingCharge(TransactionRecordMessageVO vo, String code) {
        TVinChargingOrder order = getVinOrderByCode(code);
        if (order == null) {
            return R.fail("VIN码充电订单不存在:" + code);
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_FINISHED) {
            log.warn("[VD结算] 重复账单跳过 code={}", code);
            return R.ok("重复账单,已按首次账单结算");
        }
        if (order.getStatus() != null && order.getStatus() == ORDER_STATUS_START_FAILED) {
            return R.ok("订单已关闭,跳过结算");
        }
 
        List<TransactionStageParser.PeriodElectricity> reportedPeriods =
                TransactionStageParser.parsePeriodElectricity(vo);
        BigDecimal totalElectricity = reportedPeriods.stream()
                .map(TransactionStageParser.PeriodElectricity::getElectricity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalElectricity.compareTo(BigDecimal.ZERO) <= 0 && vo.getTotal_electricity() != null) {
            totalElectricity = vo.getTotal_electricity();
        }
 
        List<TVinAccountingStrategyDetailOrder> snapshots = new ArrayList<>(vinAccountingStrategyDetailOrderService
                .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, order.getId())
                .list());
        snapshots.removeIf(item -> item.getType() == null);
        snapshots.sort(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
                Comparator.nullsLast(String::compareTo)));
 
        BigDecimal electrovalenceTotal = BigDecimal.ZERO;
        BigDecimal serviceChargeTotal = BigDecimal.ZERO;
        List<TVinChargingOrderAccountingStrategy> details = new ArrayList<>();
        // 严格对齐 CD:遍历按 start_time 排序后的策略,第 N 段只读取该策略类型对应的 chargeN。
        for (int snapshotIndex = 0; snapshotIndex < snapshots.size(); snapshotIndex++) {
            TVinAccountingStrategyDetailOrder snapshot = snapshots.get(snapshotIndex);
            BigDecimal capacity = TransactionStageParser.getPeriodElectricity(
                    vo, snapshot.getType(), snapshotIndex + 1);
            if (capacity == null || capacity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal electricPrice = snapshot.getElectrovalence() == null ? BigDecimal.ZERO : snapshot.getElectrovalence();
            BigDecimal servicePrice = snapshot.getServiceCharge() == null ? BigDecimal.ZERO : snapshot.getServiceCharge();
            BigDecimal originalServicePrice = snapshot.getCostServiceCharge() == null
                    ? BigDecimal.ZERO : snapshot.getCostServiceCharge();
            BigDecimal electricAmount = capacity.multiply(electricPrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal serviceAmount = capacity.multiply(servicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            BigDecimal originalServiceAmount = capacity.multiply(originalServicePrice).setScale(2, BigDecimal.ROUND_HALF_UP);
            electrovalenceTotal = electrovalenceTotal.add(electricAmount);
            serviceChargeTotal = serviceChargeTotal.add(serviceAmount);
 
            TVinChargingOrderAccountingStrategy detail = new TVinChargingOrderAccountingStrategy();
            detail.setChargingOrderId(order.getId());
            detail.setType(snapshot.getType());
            detail.setElectrovalence(electricPrice);
            detail.setServiceCharge(servicePrice);
            detail.setCostServiceCharge(originalServicePrice);
            detail.setChargingCapacity(capacity);
            detail.setPeriodElectricPrice(electricAmount);
            detail.setPeriodServicePrice(serviceAmount);
            detail.setPeriodOriginalServicePrice(originalServiceAmount);
            if (snapshot != null) {
                // 计费策略明细id:快照id(IdType.INPUT复制自原明细 id),与CD结算口径一致
                detail.setAccountingStrategyDetailId(snapshot.getId());
                // 时段位:与 CD 充电 TaskUtil.endOrder 同口径,截取快照时段的 HH:mm
                // (snapshot 已为 HH:mm;split 兜底兼容上游拼接的 yyyy-MM-dd HH:mm 情形)
                detail.setStartTime(splitPeriodHourMinute(snapshot.getStartTime()));
                detail.setEndTime(splitPeriodHourMinute(snapshot.getEndTime()));
            }
            // 首次添加时间:结算落库时刻(CD结算同口径)
            detail.setCreateTime(LocalDateTime.now());
            details.add(detail);
        }
        if (snapshots.isEmpty()) {
            log.error("[VD结算] 订单无计费策略快照,按0价结算并需人工核对 code={} totalElectricity={}", code, totalElectricity);
        }
        // 首段使用订单实际开始时间;订单尚未写入结束时间时,末段使用交易记录设备结束时间。
        LocalDateTime settlementStartTime = order.getStartTime() != null
                ? order.getStartTime() : parseDeviceTime(vo.getStart_time(), null);
        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 startSoc = getMonitoringSocQuietly(code, "asc");
        final BigDecimal endSoc = getMonitoringSocQuietly(code, "desc");
 
        // 本地事务:写计费明细 + 更新订单(状态5;VD后付记账模式,不做账户扣款)
        transactionTemplate.execute(status -> {
            vinChargingOrderAccountingStrategyService.lambdaUpdate()
                    .eq(TVinChargingOrderAccountingStrategy::getChargingOrderId, order.getId())
                    .remove();
            if (!details.isEmpty()) {
                vinChargingOrderAccountingStrategyService.saveBatch(details);
            }
            TVinChargingOrder update = new TVinChargingOrder();
            update.setId(order.getId());
            update.setTotalElectricity(totalElec);
            update.setChargingCapacity(totalElec);
            update.setElectrovalence(elecTotal);
            update.setServiceCharge(svcTotal);
            update.setOrderAmount(orderAmount);
            update.setPaymentAmount(orderAmount);
            if (startSoc != null) {
                update.setStartSoc(startSoc);
            }
            if (endSoc != null) {
                update.setEndSoc(endSoc);
            }
            update.setEndTime(parseDeviceTime(vo.getEnd_time(), LocalDateTime.now()));
            if (order.getStartTime() == null) {
                update.setStartTime(parseDeviceTime(vo.getStart_time(), null));
            }
            update.setStatus(ORDER_STATUS_FINISHED);
            if (order.getEndMode() == null) {
                update.setEndMode(1);
            }
            updateById(update);
            return true;
        });
 
        // 复位充电枪
        resetGunQuietly(order);
        // 推送成都监管(结算完成推枪状态 + 订单信息,与 CD 结算链路一致)
        pushRegulatoryAsync(order, false, true);
        log.info("[VD结算] 完成 code={} totalElectricity={} electrovalence={} serviceCharge={} amount={}",
                code, totalElectricity, electrovalenceTotal, serviceChargeTotal, orderAmount);
        return R.ok();
    }
 
    @Override
    public TVinChargingOrder getLatestStoppingByGunId(Integer gunId) {
        if (gunId == null) {
            return null;
        }
        return getOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getChargingGunId, gunId)
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STOPPING)
                .orderByDesc(TVinChargingOrder::getId)
                .last("limit 1"));
    }
 
    @Override
    public void vinTerminateStopByReply(PlatformStopChargingReplyVO vo) {
        // stopCharging 已前置更新状态4/结束方式1,此处仅兜底确认(幂等)
        if (vo == null) {
            return;
        }
        TChargingGun gun = null;
        try {
            GetChargingGunByCode gunQuery = new GetChargingGunByCode();
            gunQuery.setCharging_pile_code(vo.getCharging_pile_code());
            gunQuery.setCharging_gun_code(vo.getCharging_gun_code());
            gun = chargingGunClient.getChargingGunByCode(gunQuery).getData();
        } catch (Exception ex) {
            log.error("[VD停止] 查询充电枪失败 pile={} gun={}", vo.getCharging_pile_code(), vo.getCharging_gun_code(), ex);
            return;
        }
        if (gun == null) {
            return;
        }
        TVinChargingOrder order = getLatestStoppingByGunId(gun.getId());
        if (order == null) {
            return;
        }
        TVinChargingOrder update = new TVinChargingOrder();
        update.setId(order.getId());
        update.setEndMode(1);
        updateById(update);
        log.info("[VD停止] 平台停止应答兜底确认 code={}", order.getCode());
    }
 
    /**
     * 异步推送监管平台(复用 CD 约定:充电中推平台订单状态 pushOrderStatus、成都监管推枪状态
     * notificationConnectorChargeStatus / 结算完成推订单信息 notificationOrderInfo);
     * 推送失败仅告警不阻断主流程;单元测试环境 client 未注入时静默跳过
     */
    private void pushRegulatoryAsync(TVinChargingOrder order, boolean pushOrderStatus, boolean pushOrderInfo) {
        if (chargingMessageClient == null && chengDuJianGuanClient == null) {
            return;
        }
        new Thread(() -> {
            try {
                if (pushOrderStatus && chargingMessageClient != null) {
                    ChargingOrderVo chargingOrderVo = new ChargingOrderVo();
                    // 与 CD 一致使用 Spring BeanUtils(本类 ruoyi BeanUtils 同名,故用全限定名)
                    org.springframework.beans.BeanUtils.copyProperties(order, chargingOrderVo);
                    chargingMessageClient.pushOrderStatus(chargingOrderVo);
                }
                if (chengDuJianGuanClient != null) {
                    NotificationConnectorChargeStatusVo statusVo = new NotificationConnectorChargeStatusVo();
                    statusVo.setOrderCode(order.getCode());
                    statusVo.setChargingGunId(order.getChargingGunId());
                    chengDuJianGuanClient.notificationConnectorChargeStatus(statusVo);
                    if (pushOrderInfo) {
                        chengDuJianGuanClient.notificationOrderInfo(statusVo);
                    }
                }
            } catch (Exception ex) {
                log.error("[VD监管] 推送监管平台失败 code={}", order.getCode(), ex);
            }
        }, "VD-Regulatory-Push").start();
    }
 
    private void resetGunQuietly(TVinChargingOrder order) {
        updateGunStatusQuietly(order, GUN_STATUS_IDLE, "复位");
    }
 
    private boolean updateGunStatusQuietly(TVinChargingOrder order, int status, String scene) {
        if (order.getChargingGunId() == null) {
            return true;
        }
        try {
            R<TChargingGun> queryResult = chargingGunClient.getChargingGunById(order.getChargingGunId());
            TChargingGun gun = queryResult == null ? null : queryResult.getData();
            if (queryResult == null || queryResult.getCode() != R.SUCCESS || gun == null) {
                log.error("[VD资源] {}查询充电枪失败 code={} gunId={}", scene, order.getCode(), order.getChargingGunId());
                return false;
            }
            gun.setStatus(status);
            R<String> result = chargingGunClient.updateChargingGunById(gun);
            if (result == null || result.getCode() != R.SUCCESS) {
                log.error("[VD资源] {}更新充电枪失败 code={} gunId={} msg={}", scene, order.getCode(),
                        order.getChargingGunId(), result == null ? "null" : result.getMsg());
                return false;
            }
            return true;
        } catch (Exception ex) {
            log.error("[VD资源] {}更新充电枪异常 code={} gunId={}", scene, order.getCode(), order.getChargingGunId(), ex);
            return false;
        }
    }
 
    /**
     * 容错读取订单最早/最新监控记录的 SOC;Mongo 数据缺失或查询异常不阻断主结算流程。
     */
    private BigDecimal getMonitoringSocQuietly(String code, String sort) {
        if (monitoringDataService == null) {
            return null;
        }
        try {
            UploadRealTimeMonitoringData monitoringData = monitoringDataService.getDataByOrderCode(code, sort);
            return monitoringData == null || monitoringData.getSoc() == null
                    ? null : BigDecimal.valueOf(monitoringData.getSoc().longValue());
        } catch (Exception ex) {
            log.error("[VD结算] 查询监控SOC失败 code={} sort={}", code, sort, ex);
            return null;
        }
    }
 
    private LocalDateTime parseDeviceTime(String value, LocalDateTime fallback) {
        if (StringUtils.isEmpty(value)) {
            return fallback;
        }
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendPattern("yyyy-MM-dd HH:mm:ss")
                .optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd()
                .toFormatter();
        try {
            return LocalDateTime.parse(value.trim().replace('T', ' '), formatter);
        } catch (DateTimeParseException ex) {
            log.warn("[VD结算] 设备时间格式异常 value={}", value);
            return fallback;
        }
    }
 
    /**
     * 截取快照时段位 HH:mm(对齐 CD 充电 TaskUtil.endOrder 中 detailOrder.getStartTime().split(" ")[1]):
     * 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<TVinChargingOrder> listTimeoutStartingOrders() {
        return lambdaQuery()
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING)
                .lt(TVinChargingOrder::getCreateTime, LocalDateTime.now().minusMinutes(VD_STARTING_TIMEOUT_MINUTES))
                .list();
    }
 
    @Override
    public List<TVinChargingOrder> listTimeoutChargingOrders() {
        return lambdaQuery()
                .in(TVinChargingOrder::getStatus, ORDER_STATUS_CHARGING, ORDER_STATUS_STOPPING)
                .list();
    }
 
    @Override
    public void closeTimeoutStartingOrder(TVinChargingOrder order) {
        TVinChargingOrder update = new TVinChargingOrder();
        update.setStatus(ORDER_STATUS_START_FAILED);
        update.setEndMode(0);
        update.setEndTime(LocalDateTime.now());
        boolean closed = update(update, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<TVinChargingOrder>()
                .eq(TVinChargingOrder::getId, order.getId())
                .eq(TVinChargingOrder::getStatus, ORDER_STATUS_STARTING));
        if (!closed) {
            log.info("[VD补偿] 启动超时关单跳过,订单状态已变化 code={}", order.getCode());
            return;
        }
        resetGunQuietly(order);
        pushRegulatoryAsync(order, false, false);
        log.warn("[VD补偿] 启动超时关单 code={} vinId={} gunId={}", order.getCode(), order.getVinId(), order.getChargingGunId());
    }
 
    @Override
    public void compensationSettle(TVinChargingOrder order) {
        // 无设备交易记录:以订单累计电量构造全平段账单,复用统一结算入口
        // (结算锁/状态幂等/枪复位全部复用,满足文档13.3"所有路径汇聚同一结算入口")
        BigDecimal capacity = order.getChargingCapacity() != null ? order.getChargingCapacity()
                : Optional.ofNullable(order.getElectricity()).orElse(BigDecimal.ZERO);
        int flatPeriodIndex = resolveCompensationFlatPeriodIndex(order.getId());
        TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
        vo.setTransaction_serial_number(order.getCode());
        TransactionStageParser.setPeriodElectricity(vo, 3, flatPeriodIndex, capacity);
        vo.setTotal_electricity(capacity);
        log.warn("[VD补偿] 充电超时兜底结算 code={} 按平段计价 periodIndex={} capacity={}",
                order.getCode(), flatPeriodIndex, capacity);
        vinEndChargeBillingCharge(vo);
    }
 
    private int resolveCompensationFlatPeriodIndex(Long orderId) {
        List<TVinAccountingStrategyDetailOrder> snapshots = Optional.ofNullable(vinAccountingStrategyDetailOrderService
                        .lambdaQuery().eq(TVinAccountingStrategyDetailOrder::getChargingOrderId, orderId).list())
                .orElse(Collections.emptyList()).stream()
                .filter(item -> item.getType() != null)
                .sorted(Comparator.comparing(TVinAccountingStrategyDetailOrder::getStartTime,
                        Comparator.nullsLast(String::compareTo)))
                .collect(Collectors.toList());
        for (int i = 0; i < snapshots.size(); i++) {
            if (Integer.valueOf(3).equals(snapshots.get(i).getType())) {
                return i + 1;
            }
        }
        log.error("[VD补偿] 订单计费策略无平段,回退构造 flat_charge1 orderId={}", orderId);
        return 1;
    }
}