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.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());
|
result.put("maxEndTime", order.getMaxEndTime());
|
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());
|
result.put("startSoc", "-");
|
result.put("endSoc", "-");
|
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);
|
}
|
|
@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);
|
}
|
|
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 = 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;
|
// 剩余金额口径:可用金额基数(建单写入的 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);
|
}
|
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();
|
}
|
|
/**
|
* 结算刷卡账户资金(实扣冻结口径,与 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;
|
}
|
}
|