From 76f23e7d82d018e615713ca0b23835795772df7c Mon Sep 17 00:00:00 2001 From: Lemon Date: Mon, 15 Jun 2026 14:17:10 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E9=A6=96=E9=A1=B5=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E3=80=81=E6=9F=A5=E8=AF=A2=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E6=97=A5=E6=8A=A5=E8=AF=A6=E6=83=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/BusinessFinancialController.java | 57 +++ .../service/BusinessFinancialService.java | 20 + .../impl/BusinessFinancialServiceImpl.java | 390 ++++++++++++++++++ 3 files changed, 467 insertions(+) diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java index c3dbf5f90..746d2aa77 100644 --- a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java @@ -9,15 +9,19 @@ import com.jsowell.common.response.RestApiResponse; import com.google.common.collect.ImmutableMap; import com.jsowell.pile.dto.MerchantOrderReportDTO; import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO; +import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO; import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO; import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO; import com.jsowell.pile.dto.business.BusinessScaleQueryDTO; +import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO; +import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO; import com.jsowell.pile.service.BusinessFinancialService; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO; import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessOperationAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO; +import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO; import com.jsowell.pile.vo.web.MerchantOrderReportVO; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +42,59 @@ public class BusinessFinancialController extends BaseController { @Autowired private BusinessFinancialService businessFinancialService; + /** + * 查询工作台首页汇总 + * + * @param dto 查询参数。前端可选传stationIdList,运营商和日期范围由后端按登录账号解析。 + * @return 工作台首页汇总 + */ + @PostMapping("/workbenchSummary") + public RestApiResponse getWorkbenchSummary(@RequestBody(required = false) BusinessWorkbenchSummaryQueryDTO dto) { + logger.info("查询工作台首页汇总 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + BusinessWorkbenchSummaryVO result = businessFinancialService.getWorkbenchSummary(dto); + response = new RestApiResponse<>(result); + logger.info("查询工作台首页汇总成功 today:{}, stationCount:{}", + result.getToday(), result.getStationCount()); + } catch (BusinessException e) { + logger.warn("查询工作台首页汇总业务异常 code:{}, message:{}", e.getCode(), e.getMessage(), e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("查询工作台首页汇总系统异常 params:{}", JSONObject.toJSONString(dto), e); + response = new RestApiResponse<>(e); + } + return response; + } + + /** + * 查询结算日报详情 + * + * @param dto 查询参数。前端传开始日期、结束日期,可选传stationIdList。 + * @return 结算日报详情 + */ + @PostMapping("/dailySettlementDetail") + public RestApiResponse getDailySettlementDetail(@RequestBody BusinessDailySettlementQueryDTO dto) { + logger.info("查询结算日报详情 params:{}", JSONObject.toJSONString(dto)); + RestApiResponse response; + try { + if (dto == null) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + BusinessDailySettlementDetailVO result = businessFinancialService.getDailySettlementDetail(dto); + response = new RestApiResponse<>(result); + logger.info("查询结算日报详情成功 startTime:{}, endTime:{}", + dto.getStartTime(), dto.getEndTime()); + } catch (BusinessException e) { + logger.warn("查询结算日报详情业务异常 code:{}, message:{}", e.getCode(), e.getMessage(), e); + response = new RestApiResponse<>(e.getCode(), e.getMessage()); + } catch (Exception e) { + logger.error("查询结算日报详情系统异常 params:{}", JSONObject.toJSONString(dto), e); + response = new RestApiResponse<>(e); + } + return response; + } + /** * 我的钱包界面查询接口 * diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java index 87fb249f6..683d24c6e 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java @@ -3,20 +3,40 @@ package com.jsowell.pile.service; import com.jsowell.common.core.page.PageResponse; import com.jsowell.pile.dto.MerchantOrderReportDTO; import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO; +import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO; import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO; import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO; import com.jsowell.pile.dto.business.BusinessScaleQueryDTO; +import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO; +import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO; import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessOperationAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO; +import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO; import com.jsowell.pile.vo.web.MerchantOrderReportVO; import com.jsowell.pile.vo.web.ParkingCouponRecordVO; public interface BusinessFinancialService { MerchantOrderReportVO getMyWallet(MerchantOrderReportDTO dto); + /** + * 查询工作台首页汇总 + * + * @param dto 查询条件 + * @return 工作台首页汇总 + */ + BusinessWorkbenchSummaryVO getWorkbenchSummary(BusinessWorkbenchSummaryQueryDTO dto); + + /** + * 查询结算日报详情 + * + * @param dto 查询条件 + * @return 结算日报详情 + */ + BusinessDailySettlementDetailVO getDailySettlementDetail(BusinessDailySettlementQueryDTO dto); + /** * 分页查询停车优免记录 * diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java index a896626e2..6752dac78 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java @@ -12,14 +12,18 @@ import com.jsowell.common.enums.ykc.ReturnCodeEnum; import com.jsowell.common.exception.BusinessException; import com.jsowell.pile.dto.MerchantOrderReportDTO; import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO; +import com.jsowell.pile.dto.QueryOrderDTO; +import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO; import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO; import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO; import com.jsowell.pile.dto.business.BusinessOperationDateRangeDTO; import com.jsowell.pile.dto.business.BusinessOperationSummaryDTO; import com.jsowell.pile.dto.business.BusinessScaleQueryDTO; +import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO; import com.jsowell.pile.domain.PileConnectorInfo; import com.jsowell.pile.domain.PileStationInfo; import com.jsowell.pile.domain.SettleOrderReport; +import com.jsowell.pile.mapper.OrderBasicInfoMapper; import com.jsowell.pile.service.BusinessFinancialService; import com.jsowell.pile.service.CarCouponRecordService; import com.jsowell.pile.service.ClearingWithdrawInfoService; @@ -28,6 +32,7 @@ import com.jsowell.pile.service.PileStationInfoService; import com.jsowell.pile.service.SettleOrderReportService; import com.jsowell.pile.util.UserUtils; import com.jsowell.pile.vo.base.ConnectorInfoVO; +import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO; import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO; @@ -38,7 +43,9 @@ import com.jsowell.pile.vo.uniapp.business.BusinessScaleChartVO; import com.jsowell.pile.vo.uniapp.business.BusinessScaleMetricVO; import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO; import com.jsowell.pile.vo.uniapp.business.BusinessStationRankVO; +import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO; import com.jsowell.pile.vo.web.MerchantOrderReportVO; +import com.jsowell.pile.vo.web.OrderStatisticsVO; import com.jsowell.pile.vo.web.ParkingCouponRecordVO; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -52,6 +59,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -85,6 +93,18 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { private static final String EFFICIENCY_METRIC_GUN_AVG_ELECTRICITY = "GUN_AVG_ELECTRICITY"; private static final String EFFICIENCY_METRIC_POWER_UTILIZATION = "POWER_UTILIZATION"; + private static final String SETTLEMENT_ITEM_SERVICE_FEE = "CHARGE_SERVICE_FEE"; + private static final String SETTLEMENT_ITEM_ELECTRICITY_FEE = "CHARGE_ELECTRICITY_FEE"; + private static final String SETTLEMENT_ITEM_PLATFORM_SUBSIDY = "PLATFORM_SERVICE_SUBSIDY"; + private static final String SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE = "MERCHANT_PLATFORM_SERVICE_FEE"; + private static final String WORKBENCH_CARD_ORDER_MANAGEMENT = "ORDER_MANAGEMENT"; + private static final String WORKBENCH_CARD_BUSINESS_ADVISER = "BUSINESS_ADVISER"; + private static final String WORKBENCH_CARD_MY_WALLET = "MY_WALLET"; + private static final String WORKBENCH_CARD_DAILY_SETTLEMENT = "DAILY_SETTLEMENT"; + private static final String WORKBENCH_CARD_PARKING_DISCOUNT = "PARKING_DISCOUNT"; + private static final int STATION_QUERY_BATCH_SIZE = 500; + private static final int DEFAULT_DECIMAL_SCALE = 2; + /** * 财务查询通用线程池(支持多方法并行查询) * 核心线程数4:覆盖经营规模(2路)、我的钱包(3路)、经营分析(2路)、枪均效率(2路)等场景 @@ -114,6 +134,114 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { @Autowired private PileConnectorInfoService pileConnectorInfoService; + @Autowired + private OrderBasicInfoMapper orderBasicInfoMapper; + + /** + * 查询工作台首页汇总 + * + * @param dto 查询条件 + * @return 工作台首页汇总 + */ + @Override + public BusinessWorkbenchSummaryVO getWorkbenchSummary(BusinessWorkbenchSummaryQueryDTO dto) { + if (dto == null) { + dto = new BusinessWorkbenchSummaryQueryDTO(); + } + + LocalDate today = LocalDate.now(); + String todayDate = today.toString(); + String recentStartTime = today.minusDays(6).toString(); + String recentEndTime = todayDate; + + FinancialScope scope = resolveFinancialScope(dto.getStationIdList()); + + CompletableFuture todayOrderCountFuture = CompletableFuture.supplyAsync( + () -> countTodayOrderCount(scope.getStationIdList(), todayDate), FINANCIAL_THREAD_POOL); + CompletableFuture> recentReportsFuture = CompletableFuture.supplyAsync( + () -> queryRawReports(scope.getStationIdList(), recentStartTime, recentEndTime), FINANCIAL_THREAD_POOL); + + List recentReports = recentReportsFuture.join(); + ReportSummary recentSummary = buildReportSummary(recentReports); + ReportSummary todaySummary = buildReportSummary(recentReports, todayDate); + Long todayOrderCount = todayOrderCountFuture.join(); + Integer pendingParkingAppealCount = countPendingParkingAppeal(scope.getStationIdList()); + + List cardList = new ArrayList<>(); + cardList.add(buildWorkbenchCard(WORKBENCH_CARD_ORDER_MANAGEMENT, "订单管理", "今日订单量", + BigDecimal.valueOf(todayOrderCount), "单")); + cardList.add(buildWorkbenchCard(WORKBENCH_CARD_BUSINESS_ADVISER, "经营参谋", "近7天净现金流", + formatMoney(recentSummary.getOrderTotalAmount()), "元")); + cardList.add(buildWorkbenchCard(WORKBENCH_CARD_MY_WALLET, "我的钱包", "今日到账", + formatMoney(todaySummary.getArrivalAmount()), "元")); + cardList.add(buildWorkbenchCard(WORKBENCH_CARD_DAILY_SETTLEMENT, "结算日报", "近7天净现金流", + formatMoney(recentSummary.getSettlementAmount()), "元")); + cardList.add(buildWorkbenchCard(WORKBENCH_CARD_PARKING_DISCOUNT, "停车减免", "待处理申诉", + BigDecimal.valueOf(pendingParkingAppealCount), "条")); + + return BusinessWorkbenchSummaryVO.builder() + .today(todayDate) + .recentStartTime(recentStartTime) + .recentEndTime(recentEndTime) + .merchantIdList(scope.getMerchantIdList()) + .stationIdList(scope.getStationIdList()) + .stationCount(scope.getStationIdList() != null ? scope.getStationIdList().size() : null) + .todayOrderCount(todayOrderCount) + .recentNetCashFlow(formatMoney(recentSummary.getOrderTotalAmount())) + .todayArrivalAmount(formatMoney(todaySummary.getArrivalAmount())) + .recentSettlementNetCashFlow(formatMoney(recentSummary.getSettlementAmount())) + .pendingParkingAppealCount(pendingParkingAppealCount) + .cardList(cardList) + .build(); + } + + /** + * 查询结算日报详情 + * + * @param dto 查询条件 + * @return 结算日报详情 + */ + @Override + public BusinessDailySettlementDetailVO getDailySettlementDetail(BusinessDailySettlementQueryDTO dto) { + validateDailySettlementQuery(dto); + + FinancialScope scope = resolveFinancialScope(dto.getStationIdList()); + List reportList = queryRawReports(scope.getStationIdList(), dto.getStartTime(), dto.getEndTime()); + ReportSummary summary = buildReportSummary(reportList); + BigDecimal incomeTotalAmount = summary.getIncomeTotalAmount(); + BigDecimal merchantPlatformServiceFee = summary.getMerchantPlatformServiceFee(); + BigDecimal expenseTotalAmount = merchantPlatformServiceFee; + + List incomeItems = new ArrayList<>(); + incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_SERVICE_FEE, "充电服务费(元)", summary.getChargeServiceFee())); + incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_ELECTRICITY_FEE, "充电电费(元)", summary.getChargeElectricityFee())); + incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_PLATFORM_SUBSIDY, "平台给商户的服务费补贴(元)", summary.getPlatformServiceSubsidy())); + + List expenseItems = new ArrayList<>(); + expenseItems.add(buildSettlementItem(SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE, "商户给平台的服务费(元)", merchantPlatformServiceFee)); + + return BusinessDailySettlementDetailVO.builder() + .startTime(dto.getStartTime()) + .endTime(dto.getEndTime()) + .merchantIdList(scope.getMerchantIdList()) + .stationCount(scope.getStationIdList() != null ? scope.getStationIdList().size() : null) + .settlementAmount(formatMoney(summary.getSettlementAmount())) + .incomeTotalAmount(formatMoney(incomeTotalAmount)) + .expenseTotalAmount(formatMoney(expenseTotalAmount)) + .totalOrderCount(BigDecimalUtils.normalize(summary.getTotalOrderCount())) + .orderTotalAmount(formatMoney(summary.getOrderTotalAmount())) + .orderElectricity(formatQuantity(summary.getOrderElectricity())) + .chargeServiceFee(formatMoney(summary.getChargeServiceFee())) + .chargeElectricityFee(formatMoney(summary.getChargeElectricityFee())) + .platformServiceSubsidy(formatMoney(summary.getPlatformServiceSubsidy())) + .merchantPlatformServiceFee(formatMoney(merchantPlatformServiceFee)) + .tradeAmount(formatMoney(summary.getTradeAmount())) + .tradeFee(formatMoney(summary.getTradeFee())) + .incomeItems(incomeItems) + .expenseItems(expenseItems) + .build(); + } + /** * 我的钱包查询 * 优化:订单报表DB、汇付余额HTTP、提现金额DB 三个独立数据源并行查询 @@ -467,6 +595,268 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { return authorizedStationIds; } + /** + * 校验结算日报详情查询条件 + * + * @param dto 查询条件 + */ + private void validateDailySettlementQuery(BusinessDailySettlementQueryDTO dto) { + if (dto == null + || StringUtils.isBlank(dto.getStartTime()) + || StringUtils.isBlank(dto.getEndTime())) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + try { + LocalDate startDate = LocalDate.parse(dto.getStartTime()); + LocalDate endDate = LocalDate.parse(dto.getEndTime()); + if (endDate.isBefore(startDate)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + } + + /** + * 解析财务接口通用账号和站点范围 + */ + private FinancialScope resolveFinancialScope(List stationIdList) { + List merchantIdList = UserUtils.getFirstMerchantIdList(); + List queryStationIds = stationIdList; + if (CollectionUtils.isEmpty(queryStationIds)) { + if (CollectionUtils.isEmpty(merchantIdList)) { + return new FinancialScope(merchantIdList, Collections.emptyList()); + } + queryStationIds = pileStationInfoService.getStationIdsByMerchantIds(merchantIdList); + if (queryStationIds == null) { + queryStationIds = Collections.emptyList(); + } + } + return new FinancialScope(merchantIdList, resolveStationIds(queryStationIds)); + } + + /** + * 统计今日订单量 + */ + private Long countTodayOrderCount(List stationIdList, String today) { + if (stationIdList != null && stationIdList.isEmpty()) { + return 0L; + } + String startTime = today + " 00:00:00"; + String endTime = today + " 23:59:59"; + if (stationIdList == null || stationIdList.size() <= STATION_QUERY_BATCH_SIZE) { + QueryOrderDTO queryOrderDTO = QueryOrderDTO.builder() + .stationIdList(stationIdList) + .startTime(startTime) + .endTime(endTime) + .build(); + OrderStatisticsVO statistics = orderBasicInfoMapper.countBusinessOrderStatistics(queryOrderDTO); + return statistics != null && statistics.getOrderCount() != null ? statistics.getOrderCount() : 0L; + } + + long orderCount = 0L; + for (int i = 0; i < stationIdList.size(); i += STATION_QUERY_BATCH_SIZE) { + int endIndex = Math.min(i + STATION_QUERY_BATCH_SIZE, stationIdList.size()); + QueryOrderDTO queryOrderDTO = QueryOrderDTO.builder() + .stationIdList(stationIdList.subList(i, endIndex)) + .startTime(startTime) + .endTime(endTime) + .build(); + OrderStatisticsVO statistics = orderBasicInfoMapper.countBusinessOrderStatistics(queryOrderDTO); + if (statistics != null && statistics.getOrderCount() != null) { + orderCount += statistics.getOrderCount(); + } + } + return orderCount; + } + + /** + * 统计停车减免待处理申诉数 + */ + private Integer countPendingParkingAppeal(List stationIdList) { + if (stationIdList != null && stationIdList.isEmpty()) { + return 0; + } + // 当前停车减免模块暂无独立申诉状态数据源,预留工作台展示字段。 + return 0; + } + + /** + * 构建报表汇总数据 + */ + private ReportSummary buildReportSummary(List reportList) { + return buildReportSummary(reportList, null); + } + + /** + * 构建指定日期的报表汇总数据 + */ + private ReportSummary buildReportSummary(List reportList, String tradeDate) { + ReportSummary summary = new ReportSummary(); + if (CollectionUtils.isEmpty(reportList)) { + return summary; + } + for (SettleOrderReport report : reportList) { + if (StringUtils.isNotBlank(tradeDate) && !StringUtils.equals(tradeDate, report.getTradeDate())) { + continue; + } + summary.add(report); + } + return summary; + } + + /** + * 构建工作台卡片 + */ + private BusinessWorkbenchSummaryVO.WorkbenchCard buildWorkbenchCard(String cardCode, + String cardName, + String metricName, + BigDecimal metricValue, + String unit) { + return BusinessWorkbenchSummaryVO.WorkbenchCard.builder() + .cardCode(cardCode) + .cardName(cardName) + .metricName(metricName) + .metricValue(metricValue) + .unit(unit) + .build(); + } + + /** + * 构建结算项 + */ + private BusinessDailySettlementDetailVO.SettlementItem buildSettlementItem(String itemCode, + String itemName, + BigDecimal amount) { + return BusinessDailySettlementDetailVO.SettlementItem.builder() + .itemCode(itemCode) + .itemName(itemName) + .unit("元") + .amount(formatMoney(amount)) + .build(); + } + + /** + * 金额统一保留两位小数 + */ + private BigDecimal formatMoney(BigDecimal amount) { + return formatDecimal(amount); + } + + /** + * 数量统一保留两位小数 + */ + private BigDecimal formatQuantity(BigDecimal amount) { + return formatDecimal(amount); + } + + /** + * 数值统一保留两位小数 + */ + private BigDecimal formatDecimal(BigDecimal amount) { + return BigDecimalUtils.nullToZero(amount).setScale(DEFAULT_DECIMAL_SCALE, RoundingMode.HALF_UP); + } + + private static class FinancialScope { + private final List merchantIdList; + private final List stationIdList; + + private FinancialScope(List merchantIdList, List stationIdList) { + this.merchantIdList = merchantIdList; + this.stationIdList = stationIdList; + } + + private List getMerchantIdList() { + return merchantIdList; + } + + private List getStationIdList() { + return stationIdList; + } + } + + private static class ReportSummary { + private BigDecimal orderTotalAmount = BigDecimal.ZERO; + private BigDecimal totalOrderCount = BigDecimal.ZERO; + private BigDecimal orderElectricity = BigDecimal.ZERO; + private BigDecimal chargeServiceFee = BigDecimal.ZERO; + private BigDecimal chargeElectricityFee = BigDecimal.ZERO; + private BigDecimal platformServiceSubsidy = BigDecimal.ZERO; + private BigDecimal settlementAmount = BigDecimal.ZERO; + private BigDecimal tradeAmount = BigDecimal.ZERO; + private BigDecimal tradeFee = BigDecimal.ZERO; + private BigDecimal arrivalAmount = BigDecimal.ZERO; + + private void add(SettleOrderReport report) { + BigDecimal reportTotalAmount = BigDecimalUtils.nullToZero(report.getTotalAmount()); + BigDecimal reportSettleAmount = report.getSettleAmount() != null + ? report.getSettleAmount() : reportTotalAmount; + + orderTotalAmount = orderTotalAmount.add(reportTotalAmount); + totalOrderCount = totalOrderCount.add(BigDecimalUtils.parseOrZero(report.getChargeNum())); + orderElectricity = orderElectricity.add(BigDecimalUtils.nullToZero(report.getUseElectricity())); + chargeServiceFee = chargeServiceFee.add(BigDecimalUtils.nullToZero(report.getServiceAmount())); + chargeElectricityFee = chargeElectricityFee.add(BigDecimalUtils.nullToZero(report.getElectricityAmount())); + platformServiceSubsidy = platformServiceSubsidy.add(BigDecimalUtils.nullToZero(report.getVirtualAmount())); + settlementAmount = settlementAmount.add(reportSettleAmount); + tradeAmount = tradeAmount.add(BigDecimalUtils.nullToZero(report.getTradeAmount())); + tradeFee = tradeFee.add(BigDecimalUtils.nullToZero(report.getTradeFee())); + arrivalAmount = arrivalAmount.add(report.getTradeAmount() != null + ? report.getTradeAmount() : reportSettleAmount); + } + + private BigDecimal getIncomeTotalAmount() { + return chargeServiceFee.add(chargeElectricityFee).add(platformServiceSubsidy); + } + + private BigDecimal getMerchantPlatformServiceFee() { + BigDecimal fee = getIncomeTotalAmount().subtract(settlementAmount); + return fee.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : fee; + } + + private BigDecimal getOrderTotalAmount() { + return orderTotalAmount; + } + + private BigDecimal getTotalOrderCount() { + return totalOrderCount; + } + + private BigDecimal getOrderElectricity() { + return orderElectricity; + } + + private BigDecimal getChargeServiceFee() { + return chargeServiceFee; + } + + private BigDecimal getChargeElectricityFee() { + return chargeElectricityFee; + } + + private BigDecimal getPlatformServiceSubsidy() { + return platformServiceSubsidy; + } + + private BigDecimal getSettlementAmount() { + return settlementAmount; + } + + private BigDecimal getTradeAmount() { + return tradeAmount; + } + + private BigDecimal getTradeFee() { + return tradeFee; + } + + private BigDecimal getArrivalAmount() { + return arrivalAmount; + } + } + /** * 获取当前账号可访问的站点ID列表,平台账号返回null表示不限制 * From 22be4557734ce6076e0daced1c5bd8fb840ae020 Mon Sep 17 00:00:00 2001 From: Lemon Date: Mon, 15 Jun 2026 14:18:23 +0800 Subject: [PATCH 02/17] update --- .../BusinessDailySettlementQueryDTO.java | 37 +++++ .../BusinessWorkbenchSummaryQueryDTO.java | 27 ++++ .../BusinessDailySettlementDetailVO.java | 139 ++++++++++++++++++ .../business/BusinessWorkbenchSummaryVO.java | 114 ++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java new file mode 100644 index 000000000..7ffdbe5b2 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java @@ -0,0 +1,37 @@ +package com.jsowell.pile.dto.business; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 结算日报详情查询DTO + * + * @author zhangziao + * @date 2026/6/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BusinessDailySettlementQueryDTO { + + /** + * 开始日期,格式:yyyy-MM-dd + */ + private String startTime; + + /** + * 结束日期,格式:yyyy-MM-dd + */ + private String endTime; + + /** + * 站点id列表。运营商账号传当前账号下可见站点id。 + */ + private List stationIdList; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java new file mode 100644 index 000000000..dafa49b84 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java @@ -0,0 +1,27 @@ +package com.jsowell.pile.dto.business; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 工作台首页汇总查询DTO + * + * @author zhangziao + * @date 2026/6/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BusinessWorkbenchSummaryQueryDTO { + + /** + * 站点id列表。可选,不传时按当前登录账号权限查询。 + */ + private List stationIdList; + +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java new file mode 100644 index 000000000..749a345fe --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java @@ -0,0 +1,139 @@ +package com.jsowell.pile.vo.uniapp.business; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 结算日报详情VO + * + * @author zhangziao + * @date 2026/6/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BusinessDailySettlementDetailVO { + + /** + * 查询开始日期 + */ + private String startTime; + + /** + * 查询结束日期 + */ + private String endTime; + + /** + * 当前登录账号有权限的一级运营商id列表 + */ + private List merchantIdList; + + /** + * 实际参与统计的站点数量 + */ + private Integer stationCount; + + /** + * 总结算金额 + */ + private BigDecimal settlementAmount; + + /** + * 收入总金额 + */ + private BigDecimal incomeTotalAmount; + + /** + * 支出总金额 + */ + private BigDecimal expenseTotalAmount; + + /** + * 总订单数 + */ + private BigDecimal totalOrderCount; + + /** + * 订单总额 + */ + private BigDecimal orderTotalAmount; + + /** + * 订单电量 + */ + private BigDecimal orderElectricity; + + /** + * 充电服务费 + */ + private BigDecimal chargeServiceFee; + + /** + * 充电电费 + */ + private BigDecimal chargeElectricityFee; + + /** + * 平台给商户的服务费补贴 + */ + private BigDecimal platformServiceSubsidy; + + /** + * 商户给平台的服务费 + */ + private BigDecimal merchantPlatformServiceFee; + + /** + * 交易到账金额 + */ + private BigDecimal tradeAmount; + + /** + * 交易手续费 + */ + private BigDecimal tradeFee; + + /** + * 收入项列表 + */ + private List incomeItems; + + /** + * 支出项列表 + */ + private List expenseItems; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class SettlementItem { + + /** + * 项编码 + */ + private String itemCode; + + /** + * 项名称 + */ + private String itemName; + + /** + * 单位 + */ + private String unit; + + /** + * 金额 + */ + private BigDecimal amount; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java new file mode 100644 index 000000000..7c6315707 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java @@ -0,0 +1,114 @@ +package com.jsowell.pile.vo.uniapp.business; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 工作台首页汇总VO + * + * @author zhangziao + * @date 2026/6/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BusinessWorkbenchSummaryVO { + + /** + * 今日日期 + */ + private String today; + + /** + * 近7天开始日期 + */ + private String recentStartTime; + + /** + * 近7天结束日期 + */ + private String recentEndTime; + + /** + * 当前登录账号有权限的一级运营商id列表 + */ + private List merchantIdList; + + /** + * 实际参与统计的站点id列表 + */ + private List stationIdList; + + /** + * 实际参与统计的站点数量 + */ + private Integer stationCount; + + /** + * 今日订单量 + */ + private Long todayOrderCount; + + /** + * 经营参谋近7天净现金流 + */ + private BigDecimal recentNetCashFlow; + + /** + * 我的钱包今日到账 + */ + private BigDecimal todayArrivalAmount; + + /** + * 结算日报近7天净现金流 + */ + private BigDecimal recentSettlementNetCashFlow; + + /** + * 停车减免待处理申诉数 + */ + private Integer pendingParkingAppealCount; + + /** + * 首页卡片列表 + */ + private List cardList; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class WorkbenchCard { + + /** + * 卡片编码 + */ + private String cardCode; + + /** + * 卡片名称 + */ + private String cardName; + + /** + * 指标名称 + */ + private String metricName; + + /** + * 指标值 + */ + private BigDecimal metricValue; + + /** + * 单位 + */ + private String unit; + } +} From 2b040c77c59bc0011857969833849d22f6bf8c83 Mon Sep 17 00:00:00 2001 From: Lemon Date: Wed, 17 Jun 2026 15:52:33 +0800 Subject: [PATCH 03/17] =?UTF-8?q?update=20=20=E5=8F=96=E6=B6=88=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/BusinessFinancialServiceImpl.java | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java index 6752dac78..05f32d311 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java @@ -208,17 +208,13 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { FinancialScope scope = resolveFinancialScope(dto.getStationIdList()); List reportList = queryRawReports(scope.getStationIdList(), dto.getStartTime(), dto.getEndTime()); ReportSummary summary = buildReportSummary(reportList); - BigDecimal incomeTotalAmount = summary.getIncomeTotalAmount(); - BigDecimal merchantPlatformServiceFee = summary.getMerchantPlatformServiceFee(); - BigDecimal expenseTotalAmount = merchantPlatformServiceFee; List incomeItems = new ArrayList<>(); incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_SERVICE_FEE, "充电服务费(元)", summary.getChargeServiceFee())); incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_ELECTRICITY_FEE, "充电电费(元)", summary.getChargeElectricityFee())); - incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_PLATFORM_SUBSIDY, "平台给商户的服务费补贴(元)", summary.getPlatformServiceSubsidy())); - List expenseItems = new ArrayList<>(); - expenseItems.add(buildSettlementItem(SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE, "商户给平台的服务费(元)", merchantPlatformServiceFee)); + // List expenseItems = new ArrayList<>(); + // expenseItems.add(buildSettlementItem(SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE, "商户给平台的服务费(元)", merchantPlatformServiceFee)); return BusinessDailySettlementDetailVO.builder() .startTime(dto.getStartTime()) @@ -226,19 +222,15 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { .merchantIdList(scope.getMerchantIdList()) .stationCount(scope.getStationIdList() != null ? scope.getStationIdList().size() : null) .settlementAmount(formatMoney(summary.getSettlementAmount())) - .incomeTotalAmount(formatMoney(incomeTotalAmount)) - .expenseTotalAmount(formatMoney(expenseTotalAmount)) .totalOrderCount(BigDecimalUtils.normalize(summary.getTotalOrderCount())) .orderTotalAmount(formatMoney(summary.getOrderTotalAmount())) .orderElectricity(formatQuantity(summary.getOrderElectricity())) .chargeServiceFee(formatMoney(summary.getChargeServiceFee())) .chargeElectricityFee(formatMoney(summary.getChargeElectricityFee())) - .platformServiceSubsidy(formatMoney(summary.getPlatformServiceSubsidy())) - .merchantPlatformServiceFee(formatMoney(merchantPlatformServiceFee)) .tradeAmount(formatMoney(summary.getTradeAmount())) .tradeFee(formatMoney(summary.getTradeFee())) .incomeItems(incomeItems) - .expenseItems(expenseItems) + // .expenseItems(expenseItems) .build(); } @@ -783,7 +775,6 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { private BigDecimal orderElectricity = BigDecimal.ZERO; private BigDecimal chargeServiceFee = BigDecimal.ZERO; private BigDecimal chargeElectricityFee = BigDecimal.ZERO; - private BigDecimal platformServiceSubsidy = BigDecimal.ZERO; private BigDecimal settlementAmount = BigDecimal.ZERO; private BigDecimal tradeAmount = BigDecimal.ZERO; private BigDecimal tradeFee = BigDecimal.ZERO; @@ -799,7 +790,6 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { orderElectricity = orderElectricity.add(BigDecimalUtils.nullToZero(report.getUseElectricity())); chargeServiceFee = chargeServiceFee.add(BigDecimalUtils.nullToZero(report.getServiceAmount())); chargeElectricityFee = chargeElectricityFee.add(BigDecimalUtils.nullToZero(report.getElectricityAmount())); - platformServiceSubsidy = platformServiceSubsidy.add(BigDecimalUtils.nullToZero(report.getVirtualAmount())); settlementAmount = settlementAmount.add(reportSettleAmount); tradeAmount = tradeAmount.add(BigDecimalUtils.nullToZero(report.getTradeAmount())); tradeFee = tradeFee.add(BigDecimalUtils.nullToZero(report.getTradeFee())); @@ -807,15 +797,6 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { ? report.getTradeAmount() : reportSettleAmount); } - private BigDecimal getIncomeTotalAmount() { - return chargeServiceFee.add(chargeElectricityFee).add(platformServiceSubsidy); - } - - private BigDecimal getMerchantPlatformServiceFee() { - BigDecimal fee = getIncomeTotalAmount().subtract(settlementAmount); - return fee.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : fee; - } - private BigDecimal getOrderTotalAmount() { return orderTotalAmount; } @@ -836,10 +817,6 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService { return chargeElectricityFee; } - private BigDecimal getPlatformServiceSubsidy() { - return platformServiceSubsidy; - } - private BigDecimal getSettlementAmount() { return settlementAmount; } From 1b37a7d5e8cf00c52b6d9d4b1bd7923d4e0595fd Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 09:54:37 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E4=B8=8D=E8=B6=B3=E4=BB=8D=E5=8F=AF=E5=90=AF=E5=8A=A8=E5=85=85?= =?UTF-8?q?=E7=94=B5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一启动金额优先级并在启动前校验会员余额。 同步完成订单创建和余额支付后再返回鉴权成功,并增加支付凭证补偿、异常停机和结算保护。 --- .../api/uniapp/customer/PayController.java | 4 +- .../pile/util/ChargeAmountUtilsTest.java | 33 ++ .../common/constant/CacheConstants.java | 9 + .../ConfirmStartChargingRequestHandler.java | 110 ++-- .../PileApplyMergeChargeHandler.java | 3 + .../UploadRealTimeMonitorHandler.java | 23 +- .../service/rabbitmq/PileRabbitListener.java | 23 +- .../pile/mapper/OrderBasicInfoMapper.java | 6 + .../pile/service/OrderBasicInfoService.java | 27 + .../impl/OrderBasicInfoServiceImpl.java | 556 ++++++++++++++++-- .../programlogic/AbstractProgramLogic.java | 33 ++ .../DelayMerchantProgramLogic.java | 26 +- .../NotDelayMerchantProgramLogic.java | 25 +- .../jsowell/pile/util/ChargeAmountUtils.java | 35 ++ .../pile/vo/web/OrderPaymentCheckResult.java | 40 ++ .../mapper/pile/MemberBasicInfoMapper.xml | 1 + .../mapper/pile/OrderBasicInfoMapper.xml | 21 + .../com/jsowell/quartz/task/JsowellTask.java | 2 + 18 files changed, 851 insertions(+), 126 deletions(-) create mode 100644 jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java index 9879e89f3..d374086e6 100644 --- a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java +++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java @@ -8,6 +8,7 @@ import com.jsowell.common.constant.Constants; import com.jsowell.common.core.controller.BaseController; import com.jsowell.common.core.redis.RedisCache; import com.jsowell.common.enums.adapay.AdapayPayChannelEnum; +import com.jsowell.common.enums.ykc.OrderPayModeEnum; import com.jsowell.common.enums.ykc.ReturnCodeEnum; import com.jsowell.common.exception.BusinessException; import com.jsowell.common.response.RestApiResponse; @@ -125,7 +126,8 @@ public class PayController extends BaseController { if (StringUtils.isBlank(memberId)) { throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR); } - if (dto.getPayAmount() == null) { + if (dto.getPayAmount() == null + && !StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), dto.getPayMode())) { throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); } dto.setMemberId(memberId); diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java new file mode 100644 index 000000000..63ea0bd85 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java @@ -0,0 +1,33 @@ +package com.jsowell.pile.util; + +import com.jsowell.common.constant.Constants; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ChargeAmountUtilsTest { + + @Test + void resolveStartupAmount_shouldPreferFrontendAmount() { + BigDecimal amount = ChargeAmountUtils.resolveStartupAmount( + new BigDecimal("100"), new BigDecimal("300")); + + assertEquals(new BigDecimal("100"), amount); + } + + @Test + void resolveStartupAmount_shouldUseMemberDefaultWhenFrontendAmountIsNull() { + BigDecimal amount = ChargeAmountUtils.resolveStartupAmount(null, new BigDecimal("300")); + + assertEquals(new BigDecimal("300"), amount); + } + + @Test + void resolveStartupAmount_shouldUseCodeDefaultWhenOtherAmountsAreUnavailable() { + BigDecimal amount = ChargeAmountUtils.resolveStartupAmount(null, BigDecimal.ZERO); + + assertEquals(Constants.BALANCE_PAY_MAX_AMOUNT_NEW, amount); + } +} diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java index 782d37e69..14d29d455 100644 --- a/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java +++ b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java @@ -78,6 +78,15 @@ public class CacheConstants { // 余额支付订单退款锁 public static final String ORDER_BALANCE_PAYMENT_REFUND_LOCK = "order_balance_payment_refund_lock:"; + // 订单支付凭证已校验标记 + public static final String ORDER_PAYMENT_READY = "order_payment_ready:"; + + // 订单支付异常补偿锁 + public static final String ORDER_PAYMENT_RECONCILE_LOCK = "order_payment_reconcile_lock:"; + + // 订单支付异常停机冷却标记 + public static final String ORDER_PAYMENT_ANOMALY_STOP = "order_payment_anomaly_stop:"; + // 平台测试员 public static final String PLATFORM_TESTER = "platform_tester:"; diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java index c3dcef7ec..855ce3f88 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java @@ -6,29 +6,28 @@ import com.jsowell.common.constant.Constants; import com.jsowell.common.core.domain.ykc.YKCDataProtocol; import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode; import com.jsowell.common.enums.ykc.OrderPayModeEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; import com.jsowell.common.util.BytesUtil; import com.jsowell.common.util.StringUtils; import com.jsowell.common.util.YKCUtils; import com.jsowell.common.util.id.IdUtils; -import com.jsowell.common.util.spring.SpringUtils; import com.jsowell.netty.factory.YKCOperateFactory; import com.jsowell.pile.domain.PileBasicInfo; import com.jsowell.pile.dto.ConfirmStartChargingData; import com.jsowell.pile.dto.GenerateOrderDTO; import com.jsowell.pile.dto.SavePileMsgDTO; import com.jsowell.pile.service.*; +import com.jsowell.pile.util.ChargeAmountUtils; import com.jsowell.pile.vo.base.ConfirmStartChargingMemberVO; import io.netty.channel.ChannelHandlerContext; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CompletableFuture; /** * 充电桩主动申请启动充电 0x31 @@ -42,18 +41,9 @@ import java.util.concurrent.CompletableFuture; public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REQUEST_START_CHARGING_CODE.getBytes()); - // 引入线程池 - private ThreadPoolTaskExecutor executor = SpringUtils.getBean("threadPoolTaskExecutor"); - - @Autowired - private PileAuthCardService pileAuthCardService; - @Autowired private OrderBasicInfoService orderBasicInfoService; - @Autowired - private MemberPlateNumberRelationService memberPlateNumberRelationService; - @Autowired private PileMsgRecordService pileMsgRecordService; @@ -198,8 +188,14 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { .vinCode(vinCode) .build(); - // 启动充电 - Map resultMap = this.confirmStartCharging(confirmStartChargingData); + Map resultMap; + try { + // 启动充电 + resultMap = this.confirmStartCharging(confirmStartChargingData); + } catch (Exception e) { + log.error("充电桩主动申请充电处理失败, param:{}", JSON.toJSONString(confirmStartChargingData), e); + resultMap = buildFailureResult("05"); + } byte[] defeatReasonByteArr = Constants.zeroByteArray; /** @@ -279,6 +275,10 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { // 查询充电桩信息 PileBasicInfo pileBasicInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileSn); + if (pileBasicInfo == null) { + log.info("充电桩主动申请充电未查询到桩信息, pileSn:{}", pileSn); + return buildFailureResult("05"); + } // 查询是否有权限 ConfirmStartChargingMemberVO memberWalletVO = null; @@ -295,50 +295,38 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { log.info("根据vin码:{},查询会员信息:{}", vinCode, JSON.toJSONString(memberWalletVO)); } - String accountBalance = Constants.ZERO; - // 如果查询后memberWalletVO为null, 则标示无权限 if (memberWalletVO == null) { - resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); - resultMap.put("accountBalance", accountBalance); - resultMap.put("defeatReasonCode", "01"); log.info("无此会员权限"); - return resultMap; - } - - // 如果不是平台测试员, 并且也不是站点白名单, 并且总余额小于等于0, 则提示余额不足 - if (!StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag()) - && !StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag()) - && memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) { - resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); - resultMap.put("accountBalance", accountBalance); - resultMap.put("defeatReasonCode", "03"); - log.info("余额不足"); - return resultMap; + return buildFailureResult("01"); } /* 鉴权通过, 生成订单并支付 */ + BigDecimal chargeAmount; String payMode; if (StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag()) || StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())) { // 如果是平台测试员或者是站点白名单 - accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT.toString(); + chargeAmount = Constants.WHITELIST_DEFAULT_AMOUNT; payMode = OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue(); } else { - BigDecimal totalBalance = memberWalletVO.getTotalBalance(); - accountBalance = totalBalance.toString(); - if (totalBalance.compareTo(Constants.BALANCE_PAY_MAX_AMOUNT_NEW) > 0) { - accountBalance = Constants.BALANCE_PAY_MAX_AMOUNT_NEW.toString(); + chargeAmount = ChargeAmountUtils.resolveStartupAmount( + dto.getChargeAmount(), memberWalletVO.getDefaultChargeAmount()); + BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null + ? BigDecimal.ZERO + : memberWalletVO.getTotalBalance(); + if (!ChargeAmountUtils.isPositive(chargeAmount) || totalBalance.compareTo(chargeAmount) < 0) { + log.info("余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberWalletVO.getMemberId(), chargeAmount, totalBalance); + return buildFailureResult("03"); } payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(); } // 生成交易流水号, 传递给订单使用 String transactionCode = IdUtils.generateTransactionCode(pileSn, connectorCode); - resultMap.put("transactionCode", transactionCode); - resultMap.put("accountBalance", accountBalance); dto.setPileSn(pileSn); if (StringUtils.isNotBlank(memberWalletVO.getPlateNumber())) { @@ -346,34 +334,40 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { } dto.setConnectorCode(connectorCode); dto.setTransactionCode(transactionCode); - if (memberWalletVO.getDefaultChargeAmount() != null && memberWalletVO.getDefaultChargeAmount().compareTo(BigDecimal.ZERO) > 0) { - dto.setChargeAmount(memberWalletVO.getDefaultChargeAmount()); - }else { - dto.setChargeAmount(new BigDecimal(accountBalance)); - } + dto.setChargeAmount(chargeAmount); dto.setPayMode(payMode); dto.setStartMode(StringUtils.equals("01", startMode) ? "2" : "5"); dto.setMemberId(memberWalletVO.getMemberId()); dto.setMerchantId(pileBasicInfo.getMerchantId() + ""); - // 校验充电桩相关的信息 try { - orderBasicInfoService.checkPileInfoForEV(dto); + // 必须等订单创建并完成余额支付后,才能向桩端返回鉴权成功。 + Map map = orderBasicInfoService.generateOrderByCardV2(dto); + if (map == null) { + throw new BusinessException(ReturnCodeEnum.CODE_GENERATE_ORDER_ERROR); + } + } catch (BusinessException e) { + String defeatReasonCode = StringUtils.equals( + ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT.getValue(), e.getCode()) ? "03" : "05"; + log.error("充电桩主动申请充电生成订单失败, memberId:{}, chargeAmount:{}", + memberWalletVO.getMemberId(), chargeAmount, e); + return buildFailureResult(defeatReasonCode); } catch (Exception e) { - // 校验充电桩报错,返回校验值 05-桩停用 - resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); - resultMap.put("accountBalance", accountBalance); - resultMap.put("defeatReasonCode", "05"); + log.error("充电桩主动申请充电生成订单失败, memberId:{}, chargeAmount:{}", + memberWalletVO.getMemberId(), chargeAmount, e); + return buildFailureResult("05"); } - // 异步创建订单 - CompletableFuture.runAsync(() -> { - try { - Map map = orderBasicInfoService.generateOrderByCardV2(dto); - } catch (Exception e) { - throw new RuntimeException(e); - } - }, executor); + resultMap.put("transactionCode", transactionCode); + resultMap.put("accountBalance", chargeAmount.toString()); + return resultMap; + } + + private Map buildFailureResult(String defeatReasonCode) { + Map resultMap = new HashMap<>(); + resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); + resultMap.put("accountBalance", Constants.ZERO); + resultMap.put("defeatReasonCode", defeatReasonCode); return resultMap; } diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java index debe0f658..9e59c191a 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java @@ -138,6 +138,9 @@ public class PileApplyMergeChargeHandler extends AbstractYkcHandler{ log.info("桩号:{}, 并充订单鉴权成功, 结果map:{}", pileSn, map); } catch (Exception e) { log.error("桩号:{}, 并充订单鉴权失败, ", pileSn, e); + map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); + map.put("accountBalance", Constants.ZERO); + map.put("defeatReasonCode", "05"); } String transactionCode = map.get("transactionCode"); // 如果交易流水号不等于Constants.ILLEGAL_TRANSACTION_CODE, 表示鉴权通过 diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java index f7ace0826..494f080b8 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java @@ -19,6 +19,7 @@ import com.jsowell.netty.factory.YKCOperateFactory; import com.jsowell.pile.domain.OrderBasicInfo; import com.jsowell.pile.service.OrderBasicInfoService; import com.jsowell.pile.service.PileBasicInfoService; +import com.jsowell.pile.vo.web.OrderPaymentCheckResult; import com.jsowell.thirdparty.common.CommonService; import io.netty.channel.ChannelHandlerContext; import lombok.extern.slf4j.Slf4j; @@ -297,6 +298,22 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler { // 查询数据库中该订单当前信息 OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByTransactionCode(transactionCode); if (Objects.nonNull(orderInfo)) { + OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderInfo); + boolean paymentReady = paymentCheck.isOrderPaymentReady(); + if (!paymentCheck.isCheckCompleted()) { + // 查询依赖暂时异常时只保留实时数据,等待后续补偿重试,不能据此判定订单未支付。 + saveRedisFlag = true; + log.error("实时监控支付凭证校验未完成,保留实时数据且不变更订单状态, orderCode:{}, reason:{}", + orderInfo.getOrderCode(), paymentCheck.getReason()); + } else if (!paymentReady) { + // 实时监控只能保存原始数据并隔离订单,不能把待支付订单改成已支付。 + saveRedisFlag = true; + orderBasicInfoService.handleOrderPaymentAnomaly(orderInfo, + paymentCheck.getReason(), null, true); + log.warn("实时监控发现支付凭证无效, orderCode:{}, reason:{}", + orderInfo.getOrderCode(), paymentCheck.getReason()); + } + if (paymentCheck.isCheckCompleted() && paymentReady) { // 保存此订单的soc YKCUtils.saveSOC(transactionCode, realTimeMonitorData.getSOC()); @@ -315,11 +332,6 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler { orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()); } - if (StringUtils.equals(orderInfo.getPayStatus(), OrderPayStatusEnum.unpaid.getValue())) { - // 如果发现该订单的支付状态为 0-待支付,将该订单支付状态改为 1-支付完成 - orderInfo.setPayStatus(OrderPayStatusEnum.paid.getValue()); - } - // 如果原来没有开始充电时间就保存当前时间为开始充电时间 if (orderInfo.getChargeStartTime() == null) { updateFlag = true; @@ -365,6 +377,7 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler { log.error("异步绑定车辆停车优惠券error", e); } }, executor); + } } // 充电时保存实时数据到redis diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java index 60f161393..67d289af3 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java @@ -30,6 +30,7 @@ import com.jsowell.pile.service.PileBasicInfoService; import com.jsowell.pile.service.PileMsgRecordService; import com.jsowell.pile.service.YKCPushCommandService; import com.jsowell.thirdparty.common.CommonService; +import com.jsowell.pile.vo.web.OrderPaymentCheckResult; import com.rabbitmq.client.Channel; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -358,6 +359,22 @@ public class PileRabbitListener { // 查询数据库中该订单当前信息 OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByTransactionCode(transactionCode); if (Objects.nonNull(orderInfo)) { + OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderInfo); + boolean paymentReady = paymentCheck.isOrderPaymentReady(); + if (!paymentCheck.isCheckCompleted()) { + // 查询依赖暂时异常时只保留实时数据,等待后续补偿重试,不能据此判定订单未支付。 + saveRedisFlag = true; + log.error("实时监控支付凭证校验未完成,保留实时数据且不变更订单状态, orderCode:{}, reason:{}", + orderInfo.getOrderCode(), paymentCheck.getReason()); + } else if (!paymentReady) { + // 实时监控不能把待支付订单伪造成已支付,只保留实时数据并执行幂等停机。 + saveRedisFlag = true; + orderBasicInfoService.handleOrderPaymentAnomaly(orderInfo, + paymentCheck.getReason(), null, true); + log.warn("实时监控发现支付凭证无效, orderCode:{}, reason:{}", + orderInfo.getOrderCode(), paymentCheck.getReason()); + } + if (paymentCheck.isCheckCompleted() && paymentReady) { if (StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.ORDER_COMPLETE.getValue()) || StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.STAY_SETTLEMENT.getValue())) { // 在订单状态为 订单完成或待结算,不保存 @@ -373,11 +390,6 @@ public class PileRabbitListener { orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue()); } - if (StringUtils.equals(orderInfo.getPayStatus(), OrderPayStatusEnum.unpaid.getValue())) { - // 如果发现该订单的支付状态为 0-待支付,将该订单支付状态改为 1-支付完成 - orderInfo.setPayStatus(OrderPayStatusEnum.paid.getValue()); - } - // 如果原来没有开始充电时间就保存当前时间为开始充电时间 if (orderInfo.getChargeStartTime() == null) { updateFlag = true; @@ -387,6 +399,7 @@ public class PileRabbitListener { if (updateFlag) { orderBasicInfoService.updateOrderBasicInfo(orderInfo); } + } } // 充电时保存实时数据到redis diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java index ab44805da..578387bc5 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java @@ -264,6 +264,12 @@ public interface OrderBasicInfoMapper { @Param("orderStatus") String orderStatus, @Param("payStatus") String payStatus); + /** + * 查询需要进行支付凭证补偿的余额支付订单。 + */ + List queryOrdersForPaymentReconcile(@Param("startTime") String startTime, + @Param("endTime") String endTime); + /** * 个人桩查询充电数据 * diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java index 4d924a481..cdbf58f0f 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java @@ -339,6 +339,33 @@ public interface OrderBasicInfoService{ */ void closeStartFailedOrder(String startTime, String endTime); + /** + * 校验订单支付凭证,并在底层支付流水完整时修复订单主表支付字段。 + * + * @param orderBasicInfo 订单信息 + * @return 支付凭证校验结果 + */ + OrderPaymentCheckResult checkAndRepairOrderPayment(OrderBasicInfo orderBasicInfo); + + /** + * 处理支付凭证异常订单。该方法只会将订单标记为异常,不会走正常结算或退款流程。 + * + * @param orderBasicInfo 订单信息 + * @param reason 异常原因 + * @param data 原始交易记录报文,可为空 + * @param stopCharging 是否需要向充电桩发送停机指令 + */ + void handleOrderPaymentAnomaly(OrderBasicInfo orderBasicInfo, String reason, + TransactionRecordsData data, boolean stopCharging); + + /** + * 扫描并补偿支付凭证异常的余额支付订单。 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + void reconcileChargingOrderPayments(String startTime, String endTime); + List getListByOrderCodes(List orderCodes); /** diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 164848e2a..a4e743c9a 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -54,6 +54,7 @@ import com.jsowell.pile.service.programlogic.ProgramLogicFactory; import com.jsowell.pile.transaction.dto.ClearingBillTransactionDTO; import com.jsowell.pile.transaction.dto.OrderTransactionDTO; import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.util.ChargeAmountUtils; import com.jsowell.pile.util.MerchantUtils; import com.jsowell.pile.util.UserUtils; import com.jsowell.pile.vo.OrderInfoDetailVO; @@ -761,6 +762,18 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { } for (OrderBasicInfo orderBasicInfo : orderList) { + OrderPaymentCheckResult paymentCheck = checkAndRepairOrderPayment(orderBasicInfo); + if (!paymentCheck.isCheckCompleted()) { + logger.error("关闭启动失败订单前支付凭证校验未完成,本次跳过, orderCode:{}, reason:{}", + orderBasicInfo.getOrderCode(), paymentCheck.getReason()); + continue; + } + if (!paymentCheck.isOrderPaymentReady()) { + handleOrderPaymentAnomaly(orderBasicInfo, paymentCheck.getReason(), null, false); + logger.warn("关闭启动失败订单前支付凭证校验失败,跳过正常结算退款, orderCode:{}, reason:{}", + orderBasicInfo.getOrderCode(), paymentCheck.getReason()); + continue; + } // 判断支付成功时间距当前时间是否大于15分钟 String startType = orderBasicInfo.getStartType(); // 立即启动的订单使用支付时间判断,预约充电的订单使用预约时间判断 @@ -797,6 +810,428 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { } } + /** + * 校验订单支付凭证,并在支付流水完整、但订单主表字段尚未落库时进行修复。 + * + *

余额支付不能只相信 order_basic_info.pay_status。必须同时存在订单支付明细 + * 和会员钱包出账流水,且两者金额与主表支付金额一致,才允许进入充电或结算流程。

+ */ + @Override + public OrderPaymentCheckResult checkAndRepairOrderPayment(OrderBasicInfo orderBasicInfo) { + if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(false) + .orderPaymentReady(false) + .reason("订单不存在或订单号为空") + .build(); + } + + OrderPaymentCheckResult result = inspectOrderPayment(orderBasicInfo); + if (!result.isCheckCompleted()) { + return result; + } + if (result.isOrderPaymentReady()) { + redisCache.setCacheObject(CacheConstants.ORDER_PAYMENT_READY + orderBasicInfo.getOrderCode(), + Boolean.TRUE, 10, TimeUnit.MINUTES); + return result; + } + + // 只有底层流水已经形成且金额彼此一致时,才允许修复主表字段。 + if (result.isPaymentEvidenceFound() && result.getPaymentAmount() != null + && result.getPaymentAmount().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal paymentAmount = result.getPaymentAmount(); + Date paymentTime = result.getPaymentTime() == null ? DateUtils.getNowDate() : result.getPaymentTime(); + boolean needUpdate = !StringUtils.equals(OrderPayStatusEnum.paid.getValue(), orderBasicInfo.getPayStatus()) + || orderBasicInfo.getPayAmount() == null + || orderBasicInfo.getPayAmount().compareTo(paymentAmount) != 0 + || orderBasicInfo.getPayTime() == null; + if (needUpdate && orderBasicInfo.getId() != null) { + OrderBasicInfo update = new OrderBasicInfo(); + update.setId(orderBasicInfo.getId()); + update.setOrderCode(orderBasicInfo.getOrderCode()); + update.setTransactionCode(orderBasicInfo.getTransactionCode()); + update.setPayStatus(OrderPayStatusEnum.paid.getValue()); + update.setPayAmount(paymentAmount); + update.setPayTime(paymentTime); + updateOrderBasicInfo(update); + + // 同步更新当前调用链中的对象,避免后续结算仍拿到旧的 null 金额。 + orderBasicInfo.setPayStatus(OrderPayStatusEnum.paid.getValue()); + orderBasicInfo.setPayAmount(paymentAmount); + orderBasicInfo.setPayTime(paymentTime); + } + + // 再次基于修复后的主表字段校验,确保不会把部分流水误判为成功。 + result = inspectOrderPayment(orderBasicInfo); + if (result.isOrderPaymentReady()) { + redisCache.setCacheObject(CacheConstants.ORDER_PAYMENT_READY + orderBasicInfo.getOrderCode(), + Boolean.TRUE, 10, TimeUnit.MINUTES); + } + } + return result; + } + + /** + * 将支付凭证异常订单置为异常,并按幂等规则最多发送一次停机指令。 + * 此处绝不调用正常结算、退款或分账逻辑。 + */ + @Override + public void handleOrderPaymentAnomaly(OrderBasicInfo orderBasicInfo, String reason, + TransactionRecordsData data, boolean stopCharging) { + if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) { + return; + } + + String orderCode = orderBasicInfo.getOrderCode(); + String lockKey = CacheConstants.ORDER_PAYMENT_RECONCILE_LOCK + "anomaly:" + orderCode; + String requestId = IdUtils.fastSimpleUUID(); + Boolean locked = redisCache.lock(lockKey, requestId, 30); + if (!Boolean.TRUE.equals(locked)) { + logger.info("支付异常订单正在被其他线程处理, orderCode:{}", orderCode); + return; + } + + OrderBasicInfo latest = null; + boolean shouldSendStop = false; + try { + // 直接查库,避免实时监控线程拿到旧的缓存订单状态。 + latest = orderBasicInfoMapper.getOrderInfoByOrderCode(orderCode); + if (latest == null || StringUtils.equals(OrderStatusEnum.ORDER_COMPLETE.getValue(), latest.getOrderStatus())) { + return; + } + + shouldSendStop = stopCharging && StringUtils.isNotBlank(latest.getPileSn()) + && StringUtils.isNotBlank(latest.getConnectorCode()) + && StringUtils.isNotBlank(latest.getTransactionCode()); + + String anomalyReason = StringUtils.isBlank(reason) ? "订单支付凭证校验失败" : reason; + String normalizedReason = limitOrderReason("支付凭证异常:" + anomalyReason); + Date endTime = data == null ? null : DateUtils.parseDate(data.getEndTime()); + String stopReasonCode = data == null ? null : normalizeStopReasonCode(data.getStopReasonCode()); + boolean needUpdate = !StringUtils.equals(OrderStatusEnum.ABNORMAL.getValue(), latest.getOrderStatus()) + || !StringUtils.equals(normalizedReason, latest.getReason()) + || latest.getSettlementTime() == null + || latest.getChargeEndTime() == null + || (StringUtils.isNotBlank(stopReasonCode) + && !StringUtils.equals(stopReasonCode, latest.getStopReasonCode())); + if (needUpdate) { + OrderBasicInfo update = new OrderBasicInfo(); + update.setId(latest.getId()); + update.setOrderCode(latest.getOrderCode()); + update.setTransactionCode(latest.getTransactionCode()); + update.setOrderStatus(OrderStatusEnum.ABNORMAL.getValue()); + update.setReason(normalizedReason); + update.setSettlementTime(latest.getSettlementTime() == null ? DateUtils.getNowDate() : latest.getSettlementTime()); + if (latest.getChargeEndTime() == null) { + update.setChargeEndTime(endTime == null ? DateUtils.getNowDate() : endTime); + } + if (StringUtils.isNotBlank(stopReasonCode)) { + update.setStopReasonCode(stopReasonCode); + } + updateOrderBasicInfo(update); + } + + // 保存原始交易报文;服务内部按订单号去重,避免重复结算消息制造重复记录。 + if (data != null) { + savePaymentAnomalyReport(latest, data); + } + + } catch (Exception e) { + logger.error("处理支付凭证异常订单失败, orderCode:{}", orderCode, e); + } finally { + redisCache.unLock(lockKey); + } + + if (shouldSendStop && latest != null) { + String transactionCode = latest.getTransactionCode(); + String stopKey = CacheConstants.ORDER_PAYMENT_ANOMALY_STOP + transactionCode; + Boolean firstStop = redisCache.setnx(stopKey, requestId, 30 * 60); + if (Boolean.TRUE.equals(firstStop)) { + try { + pileRemoteService.remoteStopCharging(latest.getPileSn(), latest.getConnectorCode(), transactionCode); + logger.warn("支付凭证异常订单已发送停机指令, orderCode:{}, transactionCode:{}", orderCode, transactionCode); + } catch (Exception e) { + // 指令未成功下发时释放幂等标记,允许后续实时报文或定时补偿再次尝试停机。 + try { + redisCache.deleteObject(stopKey); + } catch (Exception cacheException) { + logger.error("释放支付异常停机标记失败, orderCode:{}, transactionCode:{}", + orderCode, transactionCode, cacheException); + } + logger.error("支付凭证异常订单发送停机指令失败, orderCode:{}, transactionCode:{}", orderCode, transactionCode, e); + } + } else { + logger.info("支付凭证异常订单已发送过停机指令, orderCode:{}, transactionCode:{}", orderCode, transactionCode); + } + } + } + + /** + * 定时补偿余额支付订单。补偿只修复真实存在的支付凭证;完全没有支付凭证的订单直接置为异常, + * 不会伪造支付状态,也不会进入正常结算退款。 + */ + @Override + public void reconcileChargingOrderPayments(String startTime, String endTime) { + List orderList = orderBasicInfoMapper.queryOrdersForPaymentReconcile(startTime, endTime); + if (CollectionUtils.isEmpty(orderList)) { + logger.info("{}-{}期间没有需要支付凭证补偿的订单", startTime, endTime); + return; + } + logger.info("{}-{}期间,共{}条余额支付订单需要支付凭证补偿", startTime, endTime, orderList.size()); + + for (OrderBasicInfo orderBasicInfo : orderList) { + if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) { + continue; + } + String orderCode = orderBasicInfo.getOrderCode(); + String lockKey = CacheConstants.ORDER_PAYMENT_RECONCILE_LOCK + orderCode; + String requestId = IdUtils.fastSimpleUUID(); + Boolean locked = redisCache.lock(lockKey, requestId, 30); + if (!Boolean.TRUE.equals(locked)) { + logger.info("支付凭证补偿订单正在被其他线程处理, orderCode:{}", orderCode); + continue; + } + try { + OrderBasicInfo latest = orderBasicInfoMapper.getOrderInfoByOrderCode(orderCode); + if (latest == null || StringUtils.equals(OrderStatusEnum.ORDER_COMPLETE.getValue(), latest.getOrderStatus())) { + continue; + } + OrderPaymentCheckResult result = checkAndRepairOrderPayment(latest); + if (!result.isCheckCompleted()) { + logger.error("支付凭证补偿校验未完成,本次跳过, orderCode:{}, reason:{}", + orderCode, result.getReason()); + continue; + } + if (!result.isOrderPaymentReady()) { + boolean stopCharging = isOrderStillCharging(latest); + handleOrderPaymentAnomaly(latest, result.getReason(), null, stopCharging); + } + } catch (Exception e) { + logger.error("支付凭证补偿订单处理失败, orderCode:{}", orderCode, e); + } finally { + redisCache.unLock(lockKey); + } + } + } + + private boolean isOrderStillCharging(OrderBasicInfo orderBasicInfo) { + if (orderBasicInfo == null) { + return false; + } + if (StringUtils.equals(OrderStatusEnum.IN_THE_CHARGING.getValue(), orderBasicInfo.getOrderStatus())) { + return true; + } + if (StringUtils.isBlank(orderBasicInfo.getPileSn()) || StringUtils.isBlank(orderBasicInfo.getConnectorCode()) + || StringUtils.isBlank(orderBasicInfo.getTransactionCode())) { + return false; + } + String chargingTransaction = redisCache.getCacheObject( + CacheConstants.PILE_IS_CHARGING + orderBasicInfo.getPileSn() + orderBasicInfo.getConnectorCode()); + return StringUtils.equals(orderBasicInfo.getTransactionCode(), chargingTransaction); + } + + /** + * 读取支付明细、钱包出账流水并完成一致性校验。 + */ + private OrderPaymentCheckResult inspectOrderPayment(OrderBasicInfo orderBasicInfo) { + String payMode = orderBasicInfo.getPayMode(); + String payStatus = orderBasicInfo.getPayStatus(); + if (StringUtils.equals(OrderPayStatusEnum.pay_nothing.getValue(), payStatus)) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(true) + .paymentAmount(orderBasicInfo.getPayAmount()) + .paymentTime(orderBasicInfo.getPayTime()) + .reason("订单无需支付") + .build(); + } + + List payRecords; + List walletLogs = Collections.emptyList(); + try { + payRecords = orderPayRecordService.getOrderPayRecordList(orderBasicInfo.getOrderCode()); + if (payRecords == null) { + payRecords = Collections.emptyList(); + } + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) { + List logs = memberWalletLogService.getOrderRecord( + orderBasicInfo.getOrderCode(), MemberWalletEnum.TYPE_OUT.getValue()); + walletLogs = logs == null ? Collections.emptyList() : logs; + } + } catch (Exception e) { + logger.error("读取订单支付凭证失败, orderCode:{}", orderBasicInfo.getOrderCode(), e); + return OrderPaymentCheckResult.builder() + .checkCompleted(false) + .paymentEvidenceFound(false) + .orderPaymentReady(false) + .reason("读取支付凭证失败") + .build(); + } + + BigDecimal payRecordAmount = BigDecimal.ZERO; + boolean payRecordEvidence = false; + Date paymentTime = null; + for (OrderPayRecord record : payRecords) { + if (record == null || StringUtils.equals(DelFlagEnum.DELETE.getValue(), record.getDelFlag())) { + continue; + } + boolean balanceRecord = StringUtils.equals(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), record.getPayMode()) + || StringUtils.equals(OrderPayRecordEnum.GIFT_BALANCE_PAYMENT.getValue(), record.getPayMode()); + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode) && !balanceRecord) { + continue; + } + if (record.getPayAmount() != null && record.getPayAmount().compareTo(BigDecimal.ZERO) > 0) { + payRecordEvidence = true; + payRecordAmount = payRecordAmount.add(record.getPayAmount()); + } + if (record.getCreateTime() != null && (paymentTime == null || record.getCreateTime().after(paymentTime))) { + paymentTime = record.getCreateTime(); + } + } + + BigDecimal walletAmount = BigDecimal.ZERO; + boolean walletEvidence = false; + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) { + for (MemberWalletLog walletLog : walletLogs) { + if (walletLog == null + || !StringUtils.equals(MemberWalletEnum.TYPE_OUT.getValue(), walletLog.getType()) + || !StringUtils.equals(MemberWalletEnum.SUBTYPE_PAYMENT_FOR_ORDER.getValue(), walletLog.getSubType()) + || walletLog.getAmount() == null + || walletLog.getAmount().compareTo(BigDecimal.ZERO) == 0) { + continue; + } + walletEvidence = true; + walletAmount = walletAmount.add(walletLog.getAmount().abs()); + if (walletLog.getCreateTime() != null && (paymentTime == null || walletLog.getCreateTime().after(paymentTime))) { + paymentTime = walletLog.getCreateTime(); + } + } + } + + boolean baseFieldsReady = StringUtils.equals(OrderPayStatusEnum.paid.getValue(), payStatus) + && orderBasicInfo.getPayAmount() != null + && orderBasicInfo.getPayAmount().compareTo(BigDecimal.ZERO) > 0 + && orderBasicInfo.getPayTime() != null; + + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) { + boolean evidenceFound = payRecordEvidence || walletEvidence; + if (!payRecordEvidence || !walletEvidence) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(evidenceFound) + .orderPaymentReady(false) + // 余额支付必须同时具备两类底层凭证;单边流水不能用于自动补齐主表支付状态。 + .paymentAmount(null) + .paymentTime(paymentTime) + .reason("余额支付缺少订单支付明细或会员钱包出账流水") + .build(); + } + if (!samePaymentAmount(payRecordAmount, walletAmount)) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(false) + .paymentTime(paymentTime) + .reason("订单支付明细与会员钱包出账金额不一致") + .build(); + } + if (!baseFieldsReady || !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(false) + .paymentAmount(payRecordAmount) + .paymentTime(paymentTime) + .reason("支付流水存在,但订单主表支付字段不完整或金额不一致") + .build(); + } + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(true) + .paymentAmount(payRecordAmount) + .paymentTime(paymentTime) + .reason("余额支付凭证校验通过") + .build(); + } + + // 非余额支付保持兼容:必须有完整主表支付字段;若存在支付明细,则再校验明细金额。 + if (!baseFieldsReady) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(payRecordEvidence) + .orderPaymentReady(false) + .paymentAmount(payRecordEvidence ? payRecordAmount : null) + .paymentTime(paymentTime) + .reason("订单主表支付字段不完整") + .build(); + } + if (payRecordEvidence && !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(false) + .paymentTime(paymentTime) + .reason("订单支付明细与订单主表支付金额不一致") + .build(); + } + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(true) + .paymentAmount(orderBasicInfo.getPayAmount()) + .paymentTime(orderBasicInfo.getPayTime()) + .reason("支付凭证校验通过") + .build(); + } + + private boolean samePaymentAmount(BigDecimal first, BigDecimal second) { + return first != null && second != null + && first.setScale(2, RoundingMode.HALF_UP).compareTo(second.setScale(2, RoundingMode.HALF_UP)) == 0; + } + + private String limitOrderReason(String reason) { + if (reason == null) { + return null; + } + // order_basic_info.reason 在历史库中通常为 varchar(255),原始报文另存 order_abnormal_record。 + return reason.length() > 250 ? reason.substring(0, 250) : reason; + } + + private String normalizeStopReasonCode(String stopReasonCode) { + if (StringUtils.isBlank(stopReasonCode) || StringUtils.startsWithIgnoreCase(stopReasonCode, "0x")) { + return stopReasonCode; + } + return "0x" + stopReasonCode; + } + + private void savePaymentAnomalyReport(OrderBasicInfo orderBasicInfo, TransactionRecordsData data) { + try { + OrderAbnormalRecord query = new OrderAbnormalRecord(); + query.setOrderCode(orderBasicInfo.getOrderCode()); + if (CollectionUtils.isNotEmpty(orderAbnormalRecordService.selectOrderAbnormalRecordList(query))) { + return; + } + OrderAbnormalRecord record = new OrderAbnormalRecord(); + BeanUtils.copyBeanProp(record, data); + record.setOrderCode(orderBasicInfo.getOrderCode()); + if (StringUtils.isBlank(record.getPileSn())) { + record.setPileSn(orderBasicInfo.getPileSn()); + } + if (StringUtils.isBlank(record.getConnectorCode())) { + record.setConnectorCode(orderBasicInfo.getConnectorCode()); + } + if (record.getCreateTime() == null) { + record.setCreateTime(DateUtils.getNowDate()); + } + orderAbnormalRecordService.insertOrderAbnormalRecord(record); + } catch (Exception e) { + logger.error("保存支付凭证异常原始交易报文失败, orderCode:{}", orderBasicInfo.getOrderCode(), e); + } + } + /** * 判断是否退款保险金额 * @return @@ -4115,18 +4550,7 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { } else { // 通过memberId获取账户余额 MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId); - if (memberVO == null) { - throw new BusinessException("01", "查询用户账户为空"); - } - accountBalance = memberVO.getTotalBalance(); - if (accountBalance.compareTo(BigDecimal.ZERO) <= 0) { - logger.info("充电桩主动申请启动充电生成订单 余额不足, memberId:{}, MemberVO:{}", dto.getMemberId(), JSON.toJSONString(memberVO)); - throw new BusinessException("03", "用户账户余额不足"); - } - BigDecimal defaultAmount = Constants.BALANCE_PAY_MAX_AMOUNT_NEW; - if (accountBalance.compareTo(defaultAmount) > 0) { - accountBalance = defaultAmount; - } + accountBalance = resolveBalanceChargeAmount(dto.getChargeAmount(), memberVO, dto.getMemberId()); payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(); } dto.setChargeAmount(accountBalance); @@ -4218,6 +4642,15 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { // 运营商id String merchantId = dto.getMerchantId(); + if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) { + MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId); + accountBalance = resolveBalanceChargeAmount(accountBalance, memberVO, dto.getMemberId()); + } else if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue(), payMode)) { + accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT; + } else if (!ChargeAmountUtils.isPositive(accountBalance)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR); + } + dto.setChargeAmount(accountBalance); dto.setPayMode(payMode); /* @@ -4276,6 +4709,29 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { return map; } + /** + * 确定余额支付的启动金额,并使用当前运营商钱包余额进行校验。 + */ + private BigDecimal resolveBalanceChargeAmount(BigDecimal frontendChargeAmount, MemberVO memberVO, String memberId) { + if (memberVO == null) { + throw new BusinessException("01", "查询用户账户为空"); + } + BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount( + frontendChargeAmount, memberVO.getDefaultChargeAmount()); + if (!ChargeAmountUtils.isPositive(chargeAmount)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0"); + } + BigDecimal totalBalance = memberVO.getTotalBalance() == null + ? BigDecimal.ZERO + : memberVO.getTotalBalance(); + if (totalBalance.compareTo(chargeAmount) < 0) { + logger.info("充电桩主动申请启动充电生成订单 余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberId, chargeAmount, totalBalance); + throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); + } + return chargeAmount; + } + /** * 联联平台 获取累计数据用 * @@ -6299,49 +6755,36 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { return map; } - // 如果总余额小于0, 则标示余额不足 - // if (memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) { - // map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); - // map.put("accountBalance", accountBalance); - // map.put("defeatReasonCode", "03"); - // logger.info("并充生成订单----余额不足"); - // return map; - // } - - // 如果不是平台测试员, 并且也不是站点白名单, 并且总余额小于等于0, 则提示余额不足 - if (!StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag()) - && !StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag()) - && memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) { - map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); - map.put("accountBalance", accountBalance); - map.put("defeatReasonCode", "03"); - logger.info("余额不足"); - return map; - } - /* 鉴权通过, 生成订单并支付 */ + BigDecimal chargeAmount; String payMode; if (StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag()) || StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())) { // 如果是平台测试员或者是站点白名单 - accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT.toString(); + chargeAmount = Constants.WHITELIST_DEFAULT_AMOUNT; payMode = OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue(); } else { - BigDecimal totalBalance = memberWalletVO.getTotalBalance(); - accountBalance = totalBalance.toString(); - if (totalBalance.compareTo(Constants.BALANCE_PAY_MAX_AMOUNT_NEW) > 0) { - accountBalance = Constants.BALANCE_PAY_MAX_AMOUNT_NEW.toString(); + chargeAmount = ChargeAmountUtils.resolveStartupAmount( + generateOrderDTO.getChargeAmount(), memberWalletVO.getDefaultChargeAmount()); + BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null + ? BigDecimal.ZERO + : memberWalletVO.getTotalBalance(); + if (!ChargeAmountUtils.isPositive(chargeAmount) || totalBalance.compareTo(chargeAmount) < 0) { + map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); + map.put("accountBalance", accountBalance); + map.put("defeatReasonCode", "03"); + logger.info("并充生成订单----余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberWalletVO.getMemberId(), chargeAmount, totalBalance); + return map; } payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(); } + accountBalance = chargeAmount.toString(); // 生成交易流水号, 传递给订单使用 String transactionCode = IdUtils.generateTransactionCode(pileSn, connectorCode); - map.put("transactionCode", transactionCode); - map.put("accountBalance", accountBalance); - // 异步创建订单 // GenerateOrderDTO dto = new GenerateOrderDTO(); generateOrderDTO.setPileSn(pileSn); if (StringUtils.isNotBlank(memberWalletVO.getPlateNumber())) { @@ -6350,22 +6793,35 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { generateOrderDTO.setOrderType(OrderTypeEnum.MERGE_CHARGE_ORDER.getValue()); // 并充订单标识 generateOrderDTO.setConnectorCode(connectorCode); generateOrderDTO.setTransactionCode(transactionCode); - generateOrderDTO.setChargeAmount(new BigDecimal(accountBalance)); + generateOrderDTO.setChargeAmount(chargeAmount); generateOrderDTO.setPayMode(payMode); generateOrderDTO.setStartMode(StringUtils.equals("01", startMode) ? "2" : "5"); generateOrderDTO.setMemberId(memberWalletVO.getMemberId()); generateOrderDTO.setMerchantId(pileBasicInfo.getMerchantId() + ""); - // 校验充电桩相关的信息 - checkPileInfoForEV(generateOrderDTO); - - CompletableFuture.runAsync(() -> { - try { - Map resultMap = generateOrderByCardV2(generateOrderDTO); - } catch (Exception e) { - throw new RuntimeException(e); + try { + Map resultMap = generateOrderByCardV2(generateOrderDTO); + if (resultMap == null) { + throw new BusinessException(ReturnCodeEnum.CODE_GENERATE_ORDER_ERROR); } - }, executor); + } catch (BusinessException e) { + map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); + map.put("accountBalance", Constants.ZERO); + map.put("defeatReasonCode", StringUtils.equals( + ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT.getValue(), e.getCode()) ? "03" : "05"); + logger.error("并充生成订单失败, memberId:{}, chargeAmount:{}", + memberWalletVO.getMemberId(), chargeAmount, e); + return map; + } catch (Exception e) { + map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); + map.put("accountBalance", Constants.ZERO); + map.put("defeatReasonCode", "05"); + logger.error("并充生成订单失败, memberId:{}, chargeAmount:{}", + memberWalletVO.getMemberId(), chargeAmount, e); + return map; + } + map.put("transactionCode", transactionCode); + map.put("accountBalance", accountBalance); return map; } diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java index 6b5c8181f..5f3208ab3 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java @@ -149,6 +149,30 @@ public abstract class AbstractProgramLogic implements InitializingBean { */ public abstract void settleOrder(TransactionRecordsData data, OrderBasicInfo orderBasicInfo); + /** + * 结算前统一校验支付凭证。支付凭证不完整时只隔离订单,不进入金额计算、退款或分账流程。 + */ + protected boolean ensureOrderPaymentReadyForSettlement(OrderBasicInfo orderBasicInfo, + TransactionRecordsData data) { + if (orderBasicInfo == null) { + return false; + } + OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderBasicInfo); + if (!paymentCheck.isCheckCompleted()) { + logger.error("订单支付凭证校验未完成,本次跳过结算且不变更订单状态, orderCode:{}, reason:{}", + orderBasicInfo.getOrderCode(), paymentCheck.getReason()); + return false; + } + if (!paymentCheck.isOrderPaymentReady()) { + orderBasicInfoService.handleOrderPaymentAnomaly(orderBasicInfo, + paymentCheck.getReason(), data, false); + logger.warn("订单支付凭证校验失败,跳过正常结算, orderCode:{}, reason:{}", + orderBasicInfo.getOrderCode(), paymentCheck.getReason()); + return false; + } + return true; + } + /** * 电单车结算逻辑 * @param message @@ -214,6 +238,9 @@ public abstract class AbstractProgramLogic implements InitializingBean { BigDecimal orderAmount = new BigDecimal(data.getConsumptionAmount()).setScale(2, RoundingMode.HALF_UP); // 付款金额 - 实际消费金额,如果有剩余,需要走退款操作 当使用余额支付时payAmount = principalPay + giftPay BigDecimal payAmount = orderBasicInfo.getPayAmount(); + if (payAmount == null || payAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode); + } // logger.info("结算订单:【{}】, 支付金额:{}, 消费金额:{}", orderCode, payAmount, orderAmount); // 有时候充电桩到达金额停止充电会多出一点金额,比如实际需要充50元的电,充电桩传来的消费金额为50.01元,在后台记录的时候需要舍去 @@ -280,6 +307,9 @@ public abstract class AbstractProgramLogic implements InitializingBean { public OrderDetail returnUpdateOrderDetail(OrderBasicInfo orderBasicInfo, TransactionRecordsData data) { logger.info("start更新订单详情:【{}】, orderBasicInfo:{}, TransactionRecordsData:{}", orderBasicInfo.getOrderCode(), JSON.toJSONString(orderBasicInfo), JSON.toJSONString(data)); String orderCode = orderBasicInfo.getOrderCode(); + if (orderBasicInfo.getPayAmount() == null || orderBasicInfo.getPayAmount().compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode); + } BigDecimal orderAmount = orderBasicInfo.getOrderAmount(); // 更新订单详情 查询订单详情 修改订单数据 OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderCode); @@ -480,6 +510,9 @@ public abstract class AbstractProgramLogic implements InitializingBean { String orderCode = orderBasicInfo.getOrderCode(); // 订单支付金额 BigDecimal payAmount = orderBasicInfo.getPayAmount(); + if (payAmount == null || payAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode); + } // data中的消费金额 精确到小数点后四位,包含电费、 服务费 BigDecimal dataOrderAmount = new BigDecimal(data.getConsumptionAmount()).setScale(2, RoundingMode.HALF_UP); // data中的总用电量 diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java index e1a5d3e25..53bde990d 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java @@ -32,6 +32,7 @@ import com.jsowell.pile.domain.*; import com.jsowell.pile.domain.ebike.deviceupload.EBikeMessageCmd03; import com.jsowell.pile.dto.*; import com.jsowell.pile.transaction.dto.OrderTransactionDTO; +import com.jsowell.pile.util.ChargeAmountUtils; import com.jsowell.pile.vo.uniapp.customer.MemberVO; import com.jsowell.pile.vo.uniapp.customer.PileConnectorDetailVO; import com.jsowell.pile.vo.web.BalanceDeductionAmountVO; @@ -321,14 +322,24 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); // 订单所属运营商id String merchantId = orderBasicInfo.getMerchantId(); - // 支付金额 - BigDecimal chargeAmount = dto.getPayAmount(); // 会员id String memberId = dto.getMemberId(); // 查询该会员的余额 在所属运营商的余额 MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId); + if (memberVO == null) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR); + } + // 支付金额:前端金额 > 会员默认金额 > 代码默认金额 + BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount( + dto.getPayAmount(), memberVO.getDefaultChargeAmount()); + if (!ChargeAmountUtils.isPositive(chargeAmount)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0"); + } + dto.setPayAmount(chargeAmount); // 总余额 - BigDecimal totalAccountAmount = memberVO.getTotalBalance(); + BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null + ? BigDecimal.ZERO + : memberVO.getTotalBalance(); if (totalAccountAmount.compareTo(chargeAmount) < 0) { // 总余额小于充电金额 throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); @@ -542,6 +553,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { logger.info("结算订单V2:{}, 是订单完成状态", orderBasicInfo.getOrderCode()); return; } + if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) { + return; + } // 查询订单详情 OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderBasicInfo.getOrderCode()); @@ -606,6 +620,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode()); return; } + if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, null)) { + return; + } // 查询订单信息 OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderBasicInfo.getOrderCode()); // 订单消费金额, 除了充满自停是需要计算实际消费, 其他金额都是按次收费 @@ -759,6 +776,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode()); return; } + if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) { + return; + } // 获取更新数据后的orderBasicInfo对象 returnUpdateOrderBasicInfo(orderBasicInfo, data); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java index 8235ca39d..5d6fd34e9 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java @@ -31,6 +31,7 @@ import com.jsowell.pile.domain.*; import com.jsowell.pile.domain.ebike.deviceupload.EBikeMessageCmd03; import com.jsowell.pile.dto.*; import com.jsowell.pile.transaction.dto.OrderTransactionDTO; +import com.jsowell.pile.util.ChargeAmountUtils; import com.jsowell.pile.vo.uniapp.customer.MemberVO; import com.jsowell.pile.vo.web.BalanceDeductionAmountVO; import com.jsowell.pile.vo.web.OrderDetailInfoVO; @@ -233,14 +234,24 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); // 订单所属运营商id String merchantId = orderBasicInfo.getMerchantId(); - // 支付金额 - BigDecimal chargeAmount = dto.getPayAmount(); // 会员id String memberId = dto.getMemberId(); // 查询该会员的余额 在所属运营商的余额 MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(memberId, merchantId); + if (memberVO == null) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR); + } + // 支付金额:前端金额 > 会员默认金额 > 代码默认金额 + BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount( + dto.getPayAmount(), memberVO.getDefaultChargeAmount()); + if (!ChargeAmountUtils.isPositive(chargeAmount)) { + throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0"); + } + dto.setPayAmount(chargeAmount); // 总余额 - BigDecimal totalAccountAmount = memberVO.getTotalBalance(); + BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null + ? BigDecimal.ZERO + : memberVO.getTotalBalance(); if (totalAccountAmount.compareTo(chargeAmount) < 0) { // 总余额小于充电金额 throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); @@ -251,7 +262,7 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { // 赠送金支付金额 BigDecimal giftBalancePay = BigDecimal.ZERO; // 计算下发金额 - Map stringBigDecimalMap = calculateTheAmount(memberVO); + Map stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount); if (stringBigDecimalMap != null) { principalBalancePay = stringBigDecimalMap.get("principalBalancePay"); giftBalancePay = stringBigDecimalMap.get("giftBalancePay"); @@ -427,6 +438,9 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode()); return; } + if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) { + return; + } // 获取更新数据后的orderBasicInfo对象 // returnUpdateOrderBasicInfo(orderBasicInfo, data); @@ -495,6 +509,9 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode()); return; } + if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) { + return; + } // 获取更新数据后的orderBasicInfo对象 returnUpdateOrderBasicInfo(orderBasicInfo, data); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java b/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java new file mode 100644 index 000000000..31955c1a9 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java @@ -0,0 +1,35 @@ +package com.jsowell.pile.util; + +import com.jsowell.common.constant.Constants; + +import java.math.BigDecimal; + +/** + * 启动充电金额工具类。 + */ +public final class ChargeAmountUtils { + + private ChargeAmountUtils() { + } + + /** + * 按“前端金额 > 会员默认金额 > 代码默认金额”的优先级确定启动金额。 + * + * @param frontendChargeAmount 前端传入的启动金额 + * @param memberDefaultChargeAmount 会员配置的默认启动金额 + * @return 最终启动金额 + */ + public static BigDecimal resolveStartupAmount(BigDecimal frontendChargeAmount, BigDecimal memberDefaultChargeAmount) { + if (frontendChargeAmount != null) { + return frontendChargeAmount; + } + if (isPositive(memberDefaultChargeAmount)) { + return memberDefaultChargeAmount; + } + return Constants.BALANCE_PAY_MAX_AMOUNT_NEW; + } + + public static boolean isPositive(BigDecimal amount) { + return amount != null && amount.compareTo(BigDecimal.ZERO) > 0; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java new file mode 100644 index 000000000..1cd0b87f8 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java @@ -0,0 +1,40 @@ +package com.jsowell.pile.vo.web; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单支付凭证校验结果。 + * + *

paymentEvidenceFound 表示底层支付流水是否真实存在; + * orderPaymentReady 表示订单主表字段是否已经可以安全参与后续业务。

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderPaymentCheckResult { + + /** 本次校验是否正常完成;数据库/依赖异常时为 false,不能据此判定订单未支付 */ + private boolean checkCompleted; + + /** 是否存在真实支付凭证 */ + private boolean paymentEvidenceFound; + + /** 订单支付字段是否完整且一致 */ + private boolean orderPaymentReady; + + /** 从支付流水恢复出的支付金额 */ + private BigDecimal paymentAmount; + + /** 从支付流水恢复出的支付时间 */ + private Date paymentTime; + + /** 校验失败原因 */ + private String reason; +} diff --git a/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml index 175d65e14..5d0703f4d 100644 --- a/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml +++ b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml @@ -320,6 +320,7 @@ + + + update order_basic_info set order_status = #{orderStatus,jdbcType=VARCHAR} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java index da1e6c748..fec2aabf9 100644 --- a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java @@ -134,6 +134,8 @@ public class JsowellTask { // 查询出最近2天支付成功,并且订单状态为未启动的订单 String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.addDays(new Date(), -2)); String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date()); + // 先补偿/隔离支付凭证异常订单,避免后续启动失败任务把无支付订单当成正常订单结算退款。 + orderBasicInfoService.reconcileChargingOrderPayments(startTime, endTime); orderBasicInfoService.closeStartFailedOrder(startTime, endTime); } From d3273438ba0934c62f2988c2d68698a9525936ab Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 11:49:04 +0800 Subject: [PATCH 05/17] =?UTF-8?q?update=20=E8=AE=A2=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E9=87=91=E9=A2=9D=20=3D=20=E5=85=85=E7=94=B5=E9=87=91?= =?UTF-8?q?=E9=A2=9D=20+=20=E4=BF=9D=E9=99=A9=E9=87=91=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index a4e743c9a..a74609c88 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1137,7 +1137,10 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { .reason("订单支付明细与会员钱包出账金额不一致") .build(); } - if (!baseFieldsReady || !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + // 订单支付金额 = 充电金额 + 保险金额 + BigDecimal orderPayAmount = orderBasicInfo.getPayAmount() + .add(orderBasicInfo.getInsuranceAmount() == null ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount()); + if (!baseFieldsReady || !samePaymentAmount(orderPayAmount, payRecordAmount)) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) From 2078d78768d4dc12151dcf99ff83709ad9aef4aa Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 12:46:05 +0800 Subject: [PATCH 06/17] =?UTF-8?q?update=20=E8=AE=A2=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E9=87=91=E9=A2=9D=20=3D=20=E5=85=85=E7=94=B5=E9=87=91?= =?UTF-8?q?=E9=A2=9D=20+=20=E4=BF=9D=E9=99=A9=E9=87=91=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index a74609c88..65806e58f 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1171,7 +1171,10 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { .reason("订单主表支付字段不完整") .build(); } - if (payRecordEvidence && !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + // 订单支付金额 = 充电金额 + 保险金额 + BigDecimal orderPayAmount = orderBasicInfo.getPayAmount() + .add(orderBasicInfo.getInsuranceAmount() == null ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount()); + if (payRecordEvidence && !samePaymentAmount(orderPayAmount, payRecordAmount)) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) From fcbbc52e992f435cbe9576fcf73f8222bdece2b1 Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 12:58:14 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=B7=E5=8D=A1/?= =?UTF-8?q?=E5=B9=B6=E5=85=85=E4=BD=99=E9=A2=9D=E6=A0=A1=E9=AA=8C=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=E4=BD=99=E9=A2=9D=E4=B8=8D=E8=B6=B3=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=85=85=E7=94=B5=E9=87=91=E9=A2=9D=E6=97=B6=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E5=85=A8=E9=83=A8=E4=BD=99=E9=A2=9D=EF=BC=8C=E8=80=8C?= =?UTF-8?q?=E9=9D=9E=E7=9B=B4=E6=8E=A5=E6=8B=92=E7=BB=9D=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=85=85=E7=94=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConfirmStartChargingRequestHandler: 云快充刷卡预校验,余额小于默认金额时降级为全部余额 - resolveBalanceChargeAmount: 订单生成共享校验方法,余额不足默认金额时降级为全部余额 - 并充生成订单: 同逻辑修复 - 扫码支付(NotDelayMerchant/DelayMerchant)不受影响,独立的余额校验逻辑保持不变 --- .../ConfirmStartChargingRequestHandler.java | 8 +++++++- .../service/impl/OrderBasicInfoServiceImpl.java | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java index 855ce3f88..8a66600e2 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java @@ -317,7 +317,13 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler { BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null ? BigDecimal.ZERO : memberWalletVO.getTotalBalance(); - if (!ChargeAmountUtils.isPositive(chargeAmount) || totalBalance.compareTo(chargeAmount) < 0) { + // 账户余额小于默认充电金额时,使用全部余额 + if (totalBalance.compareTo(chargeAmount) < 0) { + log.info("余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberWalletVO.getMemberId(), chargeAmount, totalBalance); + chargeAmount = totalBalance; + } + if (!ChargeAmountUtils.isPositive(chargeAmount)) { log.info("余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}", memberWalletVO.getMemberId(), chargeAmount, totalBalance); return buildFailureResult("03"); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 65806e58f..0ddddbbc2 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -4730,7 +4730,13 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { BigDecimal totalBalance = memberVO.getTotalBalance() == null ? BigDecimal.ZERO : memberVO.getTotalBalance(); + // 账户余额小于默认充电金额时,使用全部余额 if (totalBalance.compareTo(chargeAmount) < 0) { + logger.info("充电桩主动申请启动充电生成订单 余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberId, chargeAmount, totalBalance); + chargeAmount = totalBalance; + } + if (!ChargeAmountUtils.isPositive(chargeAmount)) { logger.info("充电桩主动申请启动充电生成订单 余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}", memberId, chargeAmount, totalBalance); throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); @@ -6777,7 +6783,13 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null ? BigDecimal.ZERO : memberWalletVO.getTotalBalance(); - if (!ChargeAmountUtils.isPositive(chargeAmount) || totalBalance.compareTo(chargeAmount) < 0) { + // 账户余额小于默认充电金额时,使用全部余额 + if (totalBalance.compareTo(chargeAmount) < 0) { + logger.info("并充生成订单----余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}", + memberWalletVO.getMemberId(), chargeAmount, totalBalance); + chargeAmount = totalBalance; + } + if (!ChargeAmountUtils.isPositive(chargeAmount)) { map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE); map.put("accountBalance", accountBalance); map.put("defeatReasonCode", "03"); From 2eca1f6a796d749b30c9b0b50add31d3eb1e93bf Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 13:26:27 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BF=9D=E9=99=A9=E8=AE=A2=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=87=AD=E8=AF=81=E6=A0=A1=E9=AA=8C=EF=BC=9A=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E9=94=99=E8=AF=AF=E7=9A=84=E4=BF=9D=E9=99=A9=E9=87=91?= =?UTF-8?q?=E9=A2=9D=E5=8F=8C=E9=87=8D=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit d3273438b 提交在余额支付路径也加了保险金额,但余额支付的 payAmount 已通过 payOrderSuccessCallback 累加(可能已含保险),再加一次 insuranceAmount 会导致双重计算,永远校验失败。 - 余额支付路径:直接比较 payAmount vs payRecordAmount(不加保险) - 非余额支付路径:保持 payAmount + insuranceAmount vs payRecordAmount(正确) --- .../pile/service/impl/OrderBasicInfoServiceImpl.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 0ddddbbc2..7af1df1b3 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1137,10 +1137,9 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { .reason("订单支付明细与会员钱包出账金额不一致") .build(); } - // 订单支付金额 = 充电金额 + 保险金额 - BigDecimal orderPayAmount = orderBasicInfo.getPayAmount() - .add(orderBasicInfo.getInsuranceAmount() == null ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount()); - if (!baseFieldsReady || !samePaymentAmount(orderPayAmount, payRecordAmount)) { + // 余额支付:payAmount 已通过回调累加(可能含保险),payRecordAmount 仅含余额支付记录, + // 直接比较两者即可,不应再加保险金额(避免双重计算) + if (!baseFieldsReady || !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) From 1f05471dda652287337634a28f64d18e67a76f7d Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 13:57:39 +0800 Subject: [PATCH 09/17] =?UTF-8?q?update=20=E4=BF=AE=E5=A4=8D=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/OrderBasicInfoServiceImpl.java | 17 +++++++++++++++-- .../programlogic/DelayMerchantProgramLogic.java | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 7af1df1b3..77bf0f932 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1139,14 +1139,27 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { } // 余额支付:payAmount 已通过回调累加(可能含保险),payRecordAmount 仅含余额支付记录, // 直接比较两者即可,不应再加保险金额(避免双重计算) - if (!baseFieldsReady || !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + if (!baseFieldsReady) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) .orderPaymentReady(false) .paymentAmount(payRecordAmount) .paymentTime(paymentTime) - .reason("支付流水存在,但订单主表支付字段不完整或金额不一致") + .reason("支付流水存在,但订单主表支付字段不完整(payStatus=" + payStatus + + ", payAmount=" + orderBasicInfo.getPayAmount() + + ", payTime=" + orderBasicInfo.getPayTime() + ")") + .build(); + } + if (!samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + return OrderPaymentCheckResult.builder() + .checkCompleted(true) + .paymentEvidenceFound(true) + .orderPaymentReady(false) + .paymentAmount(payRecordAmount) + .paymentTime(paymentTime) + .reason("支付流水存在,但订单主表支付金额(" + orderBasicInfo.getPayAmount() + + ")与支付明细汇总(" + payRecordAmount + ")不一致") .build(); } return OrderPaymentCheckResult.builder() diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java index 53bde990d..7f45776cc 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java @@ -356,6 +356,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { giftBalancePay = stringBigDecimalMap.get("giftBalancePay"); } + // 更新支付金额 = 本金支付金额 + 赠送金支付金额,确保与 order_pay_record 汇总一致 + chargeAmount = principalBalancePay.add(giftBalancePay); + // 更新会员钱包 全部金额都用于支付订单 UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder() .memberId(memberId) From de73b82f653e613981061a0e4739ee20d7d1ea6a Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 16:29:39 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9D=9E=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E6=94=AF=E4=BB=98=EF=BC=88=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?/=E5=9C=A8=E7=BA=BF=E6=94=AF=E4=BB=98=EF=BC=89=E4=BF=9D?= =?UTF-8?q?=E9=99=A9=E8=AE=A2=E5=8D=95=E6=94=AF=E4=BB=98=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=EF=BC=9A=E7=A7=BB=E9=99=A4=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E4=BF=9D=E9=99=A9=E9=87=91=E9=A2=9D=E5=8F=8C=E9=87=8D?= =?UTF-8?q?=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 非余额支付分支的 inspectOrderPayment 仍在 payAmount 基础上叠加 insuranceAmount 进行比对, 导致白名单/在线支付勾选保险后 payAmount + insuranceAmount ≠ payRecordAmount,订单被误判为异常。 修复: 1. 非余额分支改为直接比较 payAmount vs payRecordAmount,不再叠加 insuranceAmount 2. 错误信息拆分并打印具体值,便于定位字段不完整还是金额不一致 --- .../service/impl/OrderBasicInfoServiceImpl.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 77bf0f932..2230ff608 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1180,19 +1180,20 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { .orderPaymentReady(false) .paymentAmount(payRecordEvidence ? payRecordAmount : null) .paymentTime(paymentTime) - .reason("订单主表支付字段不完整") + .reason("订单主表支付字段不完整(payStatus=" + payStatus + + ", payAmount=" + orderBasicInfo.getPayAmount() + + ", payTime=" + orderBasicInfo.getPayTime() + ")") .build(); } - // 订单支付金额 = 充电金额 + 保险金额 - BigDecimal orderPayAmount = orderBasicInfo.getPayAmount() - .add(orderBasicInfo.getInsuranceAmount() == null ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount()); - if (payRecordEvidence && !samePaymentAmount(orderPayAmount, payRecordAmount)) { + // payAmount 已通过回调/支付记录设置(微信/支付宝/白名单等),不应再加保险金额(避免双重计算) + if (payRecordEvidence && !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) .orderPaymentReady(false) .paymentTime(paymentTime) - .reason("订单支付明细与订单主表支付金额不一致") + .reason("订单支付明细金额(" + payRecordAmount + + ")与订单主表支付金额(" + orderBasicInfo.getPayAmount() + ")不一致") .build(); } return OrderPaymentCheckResult.builder() From f11b781c276221a4439d4e475e18b92deb92ce02 Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 16:47:35 +0800 Subject: [PATCH 11/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E4=BF=9D=E9=99=A9=E8=AE=A2=E5=8D=95=20payAmo?= =?UTF-8?q?unt=20=E4=B8=8E=20payRecordAmount=20=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在线支付回调(adapayCallback)中主动将 payAmount 减去了 insuranceAmount 再传给回调, 但 order_pay_record 记录的是完整支付金额,导致校验时 payAmount(扣减后) ≠ payRecordAmount(完整)。 修复: 1. OrderService.adapayCallback: callbackDTO 传完整 amount,同时带上 insuranceAmount 2. payOrderSuccessCallback: 下发桩的启动金额用 payAmount - insuranceAmount 计算, 订单主表 payAmount 仍为完整支付金额,与 payRecordAmount 一致 --- .../main/java/com/jsowell/service/OrderService.java | 12 +++--------- .../pile/service/impl/OrderBasicInfoServiceImpl.java | 8 ++++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java index 3a889dba1..dc8a25c9f 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java @@ -1299,19 +1299,13 @@ public class OrderService { } }, executor); - // 初始化充电金额 - BigDecimal chargeAmount = amount; + // 支付订单成功 OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount(); - if (insuranceAmount.compareTo(BigDecimal.ZERO) > 0) { - // 如果用户支付了保险金额,则充电金额需将保险金额减去 - chargeAmount = amount.subtract(insuranceAmount).setScale(2, RoundingMode.HALF_UP); - } - // 支付订单成功 PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder() .orderCode(orderCode) - .payAmount(chargeAmount) // amount - // .payMode(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue()) + .payAmount(amount) // 完整支付金额(含保险),与 order_pay_record 汇总一致 + .insuranceAmount(insuranceAmount) // 保险金额,回调中用于计算下发桩的充电金额 .payMode(payModel) .acquirer(AcquirerEnum.ADAPAY.getValue()) .build(); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 2230ff608..4886cdd3e 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -5406,8 +5406,12 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { return; } - // 获取启动金额 - BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), dto.getPayAmount()); + // 获取启动金额:保险金额不计入下发桩的充电额度 + BigDecimal payAmountForPile = dto.getPayAmount(); + if (dto.getInsuranceAmount() != null && dto.getInsuranceAmount().compareTo(BigDecimal.ZERO) > 0) { + payAmountForPile = dto.getPayAmount().subtract(dto.getInsuranceAmount()); + } + BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), payAmountForPile); // 发送启动指令 if (StringUtils.equals(pileConnectorDetailVO.getChargePortType(), Constants.THREE)) { From c013f1f90d81db48ac7b944153a5b27bf348fc9f Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 17:01:41 +0800 Subject: [PATCH 12/17] =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=85=85=E7=94=B5=E6=8C=87=E4=BB=A4=E5=A2=9E=E5=8A=A0=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E7=AD=89=E5=BE=85=E5=85=85=E7=94=B5=E6=A1=A9=E5=BA=94?= =?UTF-8?q?=E7=AD=94=E5=B9=B6=E6=89=93=E5=8D=B0=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pushStartChargingCommand 发送 0x34 启机指令后,通过 SyncPromise 同步等待 0x33 应答(最多3秒),在同一线程打印启动结果。效果:成功打印成功,失败打印失败(原因),超时打印超时提示。 --- .../RemoteStartChargingRequestHandler.java | 14 ++++++++++ .../impl/YKCPushCommandServiceImpl.java | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java index e2cdd1c57..d982a8866 100644 --- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java +++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java @@ -1,5 +1,7 @@ package com.jsowell.netty.handler.yunkuaichong; +import com.jsowell.common.protocol.SyncPromise; +import com.jsowell.common.util.RpcUtil; import com.jsowell.common.constant.Constants; import com.jsowell.common.constant.mq.ThirdPartyRabbitConstants; import com.jsowell.common.core.domain.ykc.YKCDataProtocol; @@ -20,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; /** @@ -101,6 +104,17 @@ public class RemoteStartChargingRequestHandler extends AbstractYkcHandler { String failedReasonMsg = ChargingFailedReasonEnum.getMsgByCode(Integer.parseInt(failedReason, 16)); log.info("远程启动充电命令回复-交易流水号:{}, 桩编码:{}, 枪号:{}, 启动结果(00-失败, 01-成功):{}, 失败原因:{}", transactionCode, pileSn, connectorCode, startResult, failedReasonMsg); + + // 唤醒 pushStartChargingCommand 中等待应答的线程 + String responseKey = transactionCode + "_0x33"; + SyncPromise syncPromise = RpcUtil.getSyncPromiseMap().get(responseKey); + if (syncPromise != null) { + String resultMsg = StringUtils.equals(startResult, Constants.ONE) + ? "成功" + : "失败(" + failedReasonMsg + ")"; + syncPromise.setRpcResult(resultMsg.getBytes(StandardCharsets.UTF_8)); + syncPromise.wake(); + } if (StringUtils.equals(startResult, Constants.DOUBLE_ZERO)) { // 启动失败 2025年4月2日16点39分修改逻辑:启动失败后不退款, 使用支付完成未启动定时任务退款 // orderBasicInfoService.chargingPileFailedToStart(transactionCode, failedReasonMsg); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java index d4f993d75..4e511ab2d 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java @@ -406,14 +406,40 @@ public class YKCPushCommandServiceImpl implements YKCPushCommandService { byte[] accountBalanceByteArr = YKCUtils.getPriceByte(chargeAmount.toString(), 2); byte[] msgBody = Bytes.concat(orderIdByteArr, pileSnByteArr, connectorCodeByteArr, logicCardNumByteArr, physicsCardNumByteArr, accountBalanceByteArr); + + // 注册同步等待容器,用于接收充电桩 0x33 应答并打印启动结果 + SyncPromise syncPromise = new SyncPromise(); + String responseKey = transactionCode + "_0x33"; + RpcUtil.getSyncPromiseMap().put(responseKey, syncPromise); + try { this.runSend(msgBody, pileSn, YKCFrameTypeCode.REMOTE_CONTROL_START_CHARGING_CODE); } catch (Exception e) { + RpcUtil.getSyncPromiseMap().remove(responseKey); throw new RuntimeException(e); } log.info("【=====平台下发充电指令=====】:订单id:{}, 桩号:{}, 枪口号:{}, 逻辑卡号:{}, 物理卡号:{}, 账户余额:{}", transactionCode, pileSn, BytesUtil.bcd2Str(connectorCodeByteArr), logicCardNum, physicsCardNum, chargeAmount); + + // 等待充电桩应答(最多 3 秒) + byte[] response; + try { + response = syncPromise.get(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + response = null; + } finally { + RpcUtil.getSyncPromiseMap().remove(responseKey); + } + + if (response == null) { + log.warn("【启动充电结果】订单号:{}, 桩号:{}, 枪号:{}, 结果:等待充电桩响应超时(3s),桩可能未收到指令或未回复", + transactionCode, pileSn, connectorCode); + } else { + log.info("【启动充电结果】订单号:{}, 桩号:{}, 枪号:{}, 结果:{}", + transactionCode, pileSn, connectorCode, new String(response, java.nio.charset.StandardCharsets.UTF_8)); + } } /** From 7b1642f8c7926ee691ad2a7412dc3a71c884ba0e Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 18:03:15 +0800 Subject: [PATCH 13/17] =?UTF-8?q?adapayCallback=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97=EF=BC=9A=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E7=BD=91=E5=85=B3=E6=94=AF=E4=BB=98=E9=87=91=E9=A2=9D=E4=B8=8E?= =?UTF-8?q?=E4=BF=9D=E9=99=A9=E9=87=91=E9=A2=9D=EF=BC=8C=E6=96=B9=E4=BE=BF?= =?UTF-8?q?=E6=8E=92=E6=9F=A5=20payAmount=20=E4=B8=8E=20payRecordAmount=20?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/jsowell/service/OrderService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java index dc8a25c9f..43581f9da 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java @@ -1302,6 +1302,8 @@ public class OrderService { // 支付订单成功 OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount(); + log.info("adapayCallback-订单:{}, 网关支付金额(amount):{}, 订单保险金额(insuranceAmount):{}, payRecord将记录:{}", + orderCode, amount, insuranceAmount, amount); PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder() .orderCode(orderCode) .payAmount(amount) // 完整支付金额(含保险),与 order_pay_record 汇总一致 From 6904eae179712a954155618727733ac1450abaf0 Mon Sep 17 00:00:00 2001 From: Guoqs <123456@jsowell.com> Date: Sat, 25 Jul 2026 20:51:42 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BF=9D=E9=99=A9?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=B5=84=E9=87=91=E5=8F=A3=E5=BE=84=EF=BC=9A?= =?UTF-8?q?payAmount=3D=E5=85=85=E7=94=B5=E9=87=91=E9=A2=9D=EF=BC=8C?= =?UTF-8?q?=E6=98=8E=E7=BB=86=3D=E5=AE=8C=E6=95=B4=E5=AE=9E=E4=BB=98(?= =?UTF-8?q?=E5=85=85=E7=94=B5+=E4=BF=9D=E9=99=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规则:order_basic_info.pay_amount 只记充电金额(不含保险);order_pay_record 汇总为用户完整实付(充电+保险);凭证校验以 payAmount+保险==明细 为准。 - 在线支付(OrderService.adapayCallback):payAmount 存 amount-保险(充电金额), 回退 f11b781c2 的"完整金额"写入;下发桩额度直接用 payAmount,避免结算多退保险。 - 余额支付(Not/DelayMerchantProgramLogic.balancePayOrderV2):与在线一致,从钱包 扣 充电+保险,order_pay_record 记完整实付,payAmount 仍只记充电金额;余额不足 (<充电+保险)直接拒绝。会员交易流水记完整实付。 - 凭证校验(OrderBasicInfoServiceImpl.inspectOrderPayment):余额/非余额支路统一 改为容错对比 payAmount==明细 或 payAmount+保险==明细;补偿回写只写充电金额, 修复余额支路仍按 payAmount==明细 会误判新余额保险单的问题。 - 新增 docs/sql/fix_online_insurance_pay_amount_20260725.sql:矫正 f11b781c2 期间 在线保险单被写成完整金额的 payAmount(dry-run/备份/回滚齐全)。 - 新增单测:calculateBalanceRefund 6 例 + inspectOrderPayment 5 例(计算层,不连库)。 --- ...x_online_insurance_pay_amount_20260725.sql | 131 +++++++++++++++ .../com/jsowell/service/OrderService.java | 13 +- .../OrderPaymentVoucherInsuranceTest.java | 157 ++++++++++++++++++ .../BalanceInsuranceRefundTest.java | 96 +++++++++++ .../impl/OrderBasicInfoServiceImpl.java | 53 ++++-- .../DelayMerchantProgramLogic.java | 23 ++- .../NotDelayMerchantProgramLogic.java | 23 ++- 7 files changed, 462 insertions(+), 34 deletions(-) create mode 100644 docs/sql/fix_online_insurance_pay_amount_20260725.sql create mode 100644 jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java create mode 100644 jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java diff --git a/docs/sql/fix_online_insurance_pay_amount_20260725.sql b/docs/sql/fix_online_insurance_pay_amount_20260725.sql new file mode 100644 index 000000000..ed455558c --- /dev/null +++ b/docs/sql/fix_online_insurance_pay_amount_20260725.sql @@ -0,0 +1,131 @@ +-- ============================================================================ +-- 矫正在线支付「保险订单」的 pay_amount +-- 背景:提交 f11b781c2 上线到本次回退期间,在线支付回调把 order_basic_info.pay_amount +-- 写成了「完整实付(充电+保险)」。规则应为: +-- order_basic_info.pay_amount = 充电金额(不含保险) +-- order_pay_record 汇总 = 完整实付(充电 + 保险) +-- 故需把这批订单的 pay_amount 从「完整金额」矫正为「pay_amount - insurance_amount」。 +-- 适用:MySQL 5.7+(未用 CTE,5.7 可直接跑) +-- 日期:2026-07-25 +-- +-- 识别口径:在线支付(微信'4'/支付宝'5') + 已支付 + insurance>0 + pay_amount≈明细汇总。 +-- · 正确订单:pay_amount = 明细 - 保险 < 明细 +-- · 错误订单:pay_amount = 明细(含保险) => 命中本脚本 +-- +-- ⚠️ 关于「已完成(order_status='6')」订单:结算时已用错误的 pay_amount 计算过 +-- 退款(residue = pay_amount - 消费),即保险已被多退,本脚本无法追回那笔钱; +-- 对已完成订单矫正 pay_amount 只是让「记录」变正确,且会与已落库的 refund_amount +-- 不再自洽。是否处理已完成订单请按业务决定(见第 3 步的两种 WHERE)。 +-- ============================================================================ + + +-- --------------------------------------------------------------------------- +-- 第 1 步:DRY-RUN 预览(务必先只跑这个,核对数量与金额) +-- --------------------------------------------------------------------------- +SELECT + o.order_code, + o.order_status, + CASE o.order_status WHEN '6' THEN '已完成' WHEN '4' THEN '异常' ELSE '未完成' END AS status_desc, + o.pay_mode, + o.pay_amount AS cur_pay_amount, -- 当前(错误,含保险) + o.insurance_amount AS insurance_amount, + pr.pay_record_sum AS pay_record_sum, -- 明细汇总(完整实付) + (o.pay_amount - o.insurance_amount) AS corrected_pay_amount -- 矫正后(充电金额) +FROM order_basic_info o +JOIN ( + SELECT order_code, SUM(pay_amount) AS pay_record_sum + FROM order_pay_record + WHERE del_flag = '0' + GROUP BY order_code +) pr ON pr.order_code = o.order_code +WHERE o.pay_mode IN ('4','5') -- 在线支付(微信/支付宝) + AND o.pay_status = '1' -- 已支付 + AND o.insurance_amount > 0 -- 有保险 + AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005 -- pay_amount == 明细汇总 => 含了保险(错误) +ORDER BY o.order_status, o.order_code; + + +-- --------------------------------------------------------------------------- +-- 第 2 步:备份受影响行(建议执行,便于回滚) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS bak_order_pay_amount_fix_20260725 ( + order_code VARCHAR(64) PRIMARY KEY, + old_pay_amount DECIMAL(10,2), + insurance_amount DECIMAL(10,2), + new_pay_amount DECIMAL(10,2), + order_status VARCHAR(4), + backup_time DATETIME +); + +INSERT INTO bak_order_pay_amount_fix_20260725 + (order_code, old_pay_amount, insurance_amount, new_pay_amount, order_status, backup_time) +SELECT o.order_code, o.pay_amount, o.insurance_amount, + (o.pay_amount - o.insurance_amount), o.order_status, NOW() +FROM order_basic_info o +JOIN ( + SELECT order_code, SUM(pay_amount) AS pay_record_sum + FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code +) pr ON pr.order_code = o.order_code +WHERE o.pay_mode IN ('4','5') + AND o.pay_status = '1' + AND o.insurance_amount > 0 + AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005 +ON DUPLICATE KEY UPDATE backup_time = NOW(); + + +-- --------------------------------------------------------------------------- +-- 第 3 步:正式矫正(确认第 1 步无误后执行) +-- pay_amount = pay_amount - insurance_amount = 充电金额 +-- +-- ▸ 方案 A(推荐):只矫正「未完成」订单,避免动已结算/已退款的历史账 +-- —— 防止这些单将来结算时多退保险。把下面这条的注释去掉执行: +-- --------------------------------------------------------------------------- +-- UPDATE order_basic_info o +-- JOIN ( +-- SELECT order_code, SUM(pay_amount) AS pay_record_sum +-- FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code +-- ) pr ON pr.order_code = o.order_code +-- SET o.pay_amount = o.pay_amount - o.insurance_amount +-- WHERE o.pay_mode IN ('4','5') +-- AND o.pay_status = '1' +-- AND o.insurance_amount > 0 +-- AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005 +-- AND o.order_status NOT IN ('6','7'); -- 排除已完成/已关闭 + +-- ▸ 方案 B:矫正「全部」受影响订单(含已完成,仅让记录口径一致;已多退的保险无法追回) +-- 如需按此执行,去掉下面这条的注释: +-- UPDATE order_basic_info o +-- JOIN ( +-- SELECT order_code, SUM(pay_amount) AS pay_record_sum +-- FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code +-- ) pr ON pr.order_code = o.order_code +-- SET o.pay_amount = o.pay_amount - o.insurance_amount +-- WHERE o.pay_mode IN ('4','5') +-- AND o.pay_status = '1' +-- AND o.insurance_amount > 0 +-- AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005; + + +-- --------------------------------------------------------------------------- +-- 第 4 步:矫正后复核(应为 0 行) +-- --------------------------------------------------------------------------- +-- 复核逻辑:矫正后 正确订单应满足 pay_amount + insurance = 明细汇总; +-- 若仍有 pay_amount == 明细汇总 且 insurance>0,说明还有漏网的。 +SELECT COUNT(*) AS still_wrong +FROM order_basic_info o +JOIN ( + SELECT order_code, SUM(pay_amount) AS pay_record_sum + FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code +) pr ON pr.order_code = o.order_code +WHERE o.pay_mode IN ('4','5') + AND o.pay_status = '1' + AND o.insurance_amount > 0 + AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005; + + +-- --------------------------------------------------------------------------- +-- 回滚(如需):从备份表还原 +-- --------------------------------------------------------------------------- +-- UPDATE order_basic_info o +-- JOIN bak_order_pay_amount_fix_20260725 b ON b.order_code = o.order_code +-- SET o.pay_amount = b.old_pay_amount; diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java index 43581f9da..f97a12988 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java @@ -1302,12 +1302,17 @@ public class OrderService { // 支付订单成功 OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode); BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount(); - log.info("adapayCallback-订单:{}, 网关支付金额(amount):{}, 订单保险金额(insuranceAmount):{}, payRecord将记录:{}", - orderCode, amount, insuranceAmount, amount); + // 主表 payAmount 只存充电金额(不含保险):充电金额 = 网关实付(amount) - 保险金额; + // order_pay_record 仍记录完整实付(amount),故校验时 明细 == payAmount + 保险。 + BigDecimal chargeAmount = amount; + if (insuranceAmount != null && insuranceAmount.compareTo(BigDecimal.ZERO) > 0) { + chargeAmount = amount.subtract(insuranceAmount).setScale(2, RoundingMode.HALF_UP); + } + log.info("adapayCallback-订单:{}, 网关支付金额(amount):{}, 订单保险金额(insuranceAmount):{}, 充电金额(payAmount将记录):{}, order_pay_record记录:{}", + orderCode, amount, insuranceAmount, chargeAmount, amount); PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder() .orderCode(orderCode) - .payAmount(amount) // 完整支付金额(含保险),与 order_pay_record 汇总一致 - .insuranceAmount(insuranceAmount) // 保险金额,回调中用于计算下发桩的充电金额 + .payAmount(chargeAmount) // 充电金额(不含保险) .payMode(payModel) .acquirer(AcquirerEnum.ADAPAY.getValue()) .build(); diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java new file mode 100644 index 000000000..267bc7176 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java @@ -0,0 +1,157 @@ +package com.jsowell.pile.service.impl; + +import com.jsowell.common.enums.DelFlagEnum; +import com.jsowell.common.enums.MemberWalletEnum; +import com.jsowell.common.enums.ykc.OrderPayModeEnum; +import com.jsowell.common.enums.ykc.OrderPayRecordEnum; +import com.jsowell.common.enums.ykc.OrderPayStatusEnum; +import com.jsowell.pile.domain.MemberWalletLog; +import com.jsowell.pile.domain.OrderBasicInfo; +import com.jsowell.pile.domain.OrderPayRecord; +import com.jsowell.pile.service.MemberWalletLogService; +import com.jsowell.pile.service.OrderPayRecordService; +import com.jsowell.pile.vo.web.OrderPaymentCheckResult; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.objenesis.ObjenesisStd; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * 支付凭证校验({@code inspectOrderPayment})保险口径的单元测试(mock 依赖,不连库)。 + * + *

规则:主表 payAmount = 充电金额;order_pay_record 汇总 = 完整实付(充电 + 保险); + * {@code payAmount==明细 或 payAmount+保险==明细} 任一成立即视为一致。

+ */ +class OrderPaymentVoucherInsuranceTest { + + private static final String ORDER_CODE = "O-TEST"; + + private static void setField(Object target, String name, Object value) { + try { + Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(target, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static OrderPaymentCheckResult inspect(OrderBasicInfoServiceImpl service, OrderBasicInfo order) { + try { + Method m = OrderBasicInfoServiceImpl.class.getDeclaredMethod("inspectOrderPayment", OrderBasicInfo.class); + m.setAccessible(true); + return (OrderPaymentCheckResult) m.invoke(service, order); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static OrderPayRecord payRecord(String payMode, String amount) { + return OrderPayRecord.builder() + .payMode(payMode) + .payAmount(new BigDecimal(amount)) + .delFlag(DelFlagEnum.NORMAL.getValue()) + .createTime(new Date()) + .build(); + } + + private static MemberWalletLog walletOut(String amount) { + return MemberWalletLog.builder() + .type(MemberWalletEnum.TYPE_OUT.getValue()) + .subType(MemberWalletEnum.SUBTYPE_PAYMENT_FOR_ORDER.getValue()) + .amount(new BigDecimal(amount)) + .createTime(new Date()) + .build(); + } + + private static OrderBasicInfo order(String payMode, String payAmount, String insurance) { + OrderBasicInfo o = new OrderBasicInfo(); + o.setOrderCode(ORDER_CODE); + o.setPayMode(payMode); + o.setPayStatus(OrderPayStatusEnum.paid.getValue()); + o.setPayAmount(new BigDecimal(payAmount)); + o.setPayTime(new Date()); + o.setInsuranceAmount(new BigDecimal(insurance)); + return o; + } + + private OrderBasicInfoServiceImpl serviceWith(List records, List walletLogs) { + OrderPayRecordService payRecordService = Mockito.mock(OrderPayRecordService.class); + MemberWalletLogService walletLogService = Mockito.mock(MemberWalletLogService.class); + when(payRecordService.getOrderPayRecordList(ORDER_CODE)).thenReturn(records); + when(walletLogService.getOrderRecord(ORDER_CODE, MemberWalletEnum.TYPE_OUT.getValue())).thenReturn(walletLogs); + // OrderBasicInfoServiceImpl 构造器/字段初始化里有 SpringUtils.getBean,依赖 Spring 容器; + // 用 Objenesis 绕过构造器实例化,只注入本用例需要的依赖。 + OrderBasicInfoServiceImpl service = new ObjenesisStd().newInstance(OrderBasicInfoServiceImpl.class); + setField(service, "logger", LoggerFactory.getLogger(OrderPaymentVoucherInsuranceTest.class)); + setField(service, "orderPayRecordService", payRecordService); + setField(service, "memberWalletLogService", walletLogService); + return service; + } + + @Test + void balanceInsuranceOrder_isConsistent() { + // 余额:payAmount=50(充电),明细=50.5(充电+保险),保险0.5 → 50+0.5==50.5 ✓ + OrderBasicInfoServiceImpl service = serviceWith( + Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50.5")), + Collections.singletonList(walletOut("-50.5"))); + OrderPaymentCheckResult r = inspect(service, + order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "50.00", "0.5")); + assertTrue(r.isOrderPaymentReady(), r.getReason()); + } + + @Test + void balanceNoInsurance_isConsistent() { + // 余额无保险:payAmount=50,明细=50 → 50==50 ✓(回归) + OrderBasicInfoServiceImpl service = serviceWith( + Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50")), + Collections.singletonList(walletOut("-50"))); + OrderPaymentCheckResult r = inspect(service, + order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "50", "0")); + assertTrue(r.isOrderPaymentReady(), r.getReason()); + } + + @Test + void balanceGenuineMismatch_isFlagged() { + // 余额真异常:payAmount=40,明细=50.5,保险0.5 → 40≠50.5 且 40.5≠50.5 → 不通过 + OrderBasicInfoServiceImpl service = serviceWith( + Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50.5")), + Collections.singletonList(walletOut("-50.5"))); + OrderPaymentCheckResult r = inspect(service, + order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "40", "0.5")); + assertFalse(r.isOrderPaymentReady()); + } + + @Test + void onlineInsuranceOrder_isConsistent() { + // 在线:payAmount=50(充电),明细=50.5,保险0.5 → 50+0.5==50.5 ✓ + OrderBasicInfoServiceImpl service = serviceWith( + Collections.singletonList(payRecord(OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue(), "50.5")), + Collections.emptyList()); + OrderPaymentCheckResult r = inspect(service, + order(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue(), "50.00", "0.5")); + assertTrue(r.isOrderPaymentReady(), r.getReason()); + } + + @Test + void onlineLegacyFullPayAmount_isConsistent() { + // 兼容 f11b781c2 期间历史单:payAmount=50.5(含保险),明细=50.5 → 50.5==50.5 ✓ + OrderBasicInfoServiceImpl service = serviceWith( + Collections.singletonList(payRecord(OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue(), "50.5")), + Collections.emptyList()); + OrderPaymentCheckResult r = inspect(service, + order(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue(), "50.5", "0.5")); + assertTrue(r.isOrderPaymentReady(), r.getReason()); + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java new file mode 100644 index 000000000..43386580b --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java @@ -0,0 +1,96 @@ +package com.jsowell.pile.service.programlogic; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 余额支付「电费 + 保险费」退款计算的单元测试(纯计算,不连库)。 + * + *

前提:order_pay_record 汇总(principalPay + giftPay) = 完整实付(充电 + 保险), + * 故 {@code 电费预付款 = 总支付 - 保险}。这是「余额与在线一样从钱包扣保险」之后才成立的口径。

+ * + *

覆盖场景:

+ *
    + *
  1. 无保险 + 正常消费(回归,行为不变)
  2. + *
  3. 有保险 + 正常消费(消费≥1) → 保险不退
  4. + *
  5. 有保险 + 消费<1 → 保险退
  6. + *
  7. 保险跨本金/赠金拆分 + 消费<1
  8. + *
  9. 折扣把实际电费消费压到<1 → 保险退
  10. + *
  11. 消费恰好=1(边界)→ 保险不退
  12. + *
+ */ +class BalanceInsuranceRefundTest { + + private static Map refund(String principalPay, String giftPay, + String orderAmount, String discount, String insurance) { + try { + Method method = AbstractProgramLogic.class.getDeclaredMethod("calculateBalanceRefund", + BigDecimal.class, BigDecimal.class, BigDecimal.class, BigDecimal.class, BigDecimal.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(null, + new BigDecimal(principalPay), new BigDecimal(giftPay), new BigDecimal(orderAmount), + new BigDecimal(discount), new BigDecimal(insurance)); + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void assertAmount(String key, String expected, BigDecimal actual) { + assertTrue(actual != null && new BigDecimal(expected).compareTo(actual) == 0, + key + " 期望 " + expected + " 实际 " + actual); + } + + /** electricityPrincipal/Gift = 电费退款;insurancePrincipal/Gift = 保险退款。 */ + private static void assertRefund(Map m, + String electricityPrincipal, String electricityGift, + String insurancePrincipal, String insuranceGift) { + assertAmount("returnPrincipal", electricityPrincipal, m.get("returnPrincipal")); + assertAmount("returnGift", electricityGift, m.get("returnGift")); + assertAmount("returnPrincipalForInsurance", insurancePrincipal, m.get("returnPrincipalForInsurance")); + assertAmount("returnGiftForInsurance", insuranceGift, m.get("returnGiftForInsurance")); + } + + @Test + void noInsurance_normalConsumption_refundUnusedElectricityOnly() { + // 本金50全充电,消费30 → 退20;无保险 + assertRefund(refund("50", "0", "30", "0", "0"), "20", "0", "0", "0"); + } + + @Test + void withInsurance_normalConsumption_insuranceNotRefunded() { + // 实付50.5(充电50+保险0.5),消费30(≥1) → 电费退20,保险不退 + assertRefund(refund("50.5", "0", "30", "0", "0.5"), "20", "0", "0", "0"); + } + + @Test + void withInsurance_lowConsumption_insuranceRefunded() { + // 实付50.5,消费0.5(<1) → 电费退49.5,保险退0.5 + assertRefund(refund("50.5", "0", "0.5", "0", "0.5"), "49.5", "0", "0.5", "0"); + } + + @Test + void insuranceSplitAcrossPrincipalAndGift_lowConsumption() { + // 本金0.3+赠金50.2=50.5;保险0.5优先扣本金0.3、赠金补0.2;消费0.2(<1) + // 电费:本金退0,赠金退49.8;保险退:本金0.3、赠金0.2 + assertRefund(refund("0.3", "50.2", "0.2", "0", "0.5"), "0", "49.8", "0.3", "0.2"); + } + + @Test + void discountReducesConsumptionBelowOne_insuranceRefunded() { + // 消费10、折扣9.5 → 实际电费消费0.5(<1) → 电费退49.5,保险退0.5 + assertRefund(refund("50.5", "0", "10", "9.5", "0.5"), "49.5", "0", "0.5", "0"); + } + + @Test + void consumptionExactlyOne_insuranceNotRefunded() { + // 实际电费消费=1.0,非严格<1 → 保险不退;电费退49 + assertRefund(refund("50.5", "0", "1", "0", "0.5"), "49", "0", "0", "0"); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java index 4886cdd3e..2424f17e0 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java @@ -1137,29 +1137,36 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { .reason("订单支付明细与会员钱包出账金额不一致") .build(); } - // 余额支付:payAmount 已通过回调累加(可能含保险),payRecordAmount 仅含余额支付记录, - // 直接比较两者即可,不应再加保险金额(避免双重计算) + // 余额支付主表 payAmount 只记充电金额;order_pay_record/钱包出账为完整实付(充电+保险)。 + // 故 payAmount==明细 或 payAmount+保险==明细 任一成立即视为一致(无保险时两者等价,兼容历史单)。 + BigDecimal balanceInsuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount()); + BigDecimal balanceChargeOnly = payRecordAmount.subtract(balanceInsuranceAmount); + if (balanceChargeOnly.compareTo(BigDecimal.ZERO) < 0) { + balanceChargeOnly = BigDecimal.ZERO; + } if (!baseFieldsReady) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) .orderPaymentReady(false) - .paymentAmount(payRecordAmount) + .paymentAmount(balanceChargeOnly) // 修复回写只写充电金额(明细-保险) .paymentTime(paymentTime) .reason("支付流水存在,但订单主表支付字段不完整(payStatus=" + payStatus + ", payAmount=" + orderBasicInfo.getPayAmount() + ", payTime=" + orderBasicInfo.getPayTime() + ")") .build(); } - if (!samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + boolean balanceAmountConsistent = samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount) + || samePaymentAmount(orderBasicInfo.getPayAmount().add(balanceInsuranceAmount), payRecordAmount); + if (!balanceAmountConsistent) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) .orderPaymentReady(false) - .paymentAmount(payRecordAmount) + .paymentAmount(balanceChargeOnly) .paymentTime(paymentTime) .reason("支付流水存在,但订单主表支付金额(" + orderBasicInfo.getPayAmount() - + ")与支付明细汇总(" + payRecordAmount + ")不一致") + + ")+保险金额(" + balanceInsuranceAmount + ")与支付明细汇总(" + payRecordAmount + ")不一致") .build(); } return OrderPaymentCheckResult.builder() @@ -1174,26 +1181,44 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { // 非余额支付保持兼容:必须有完整主表支付字段;若存在支付明细,则再校验明细金额。 if (!baseFieldsReady) { + // 补偿修复回写主表 payAmount 时只写充电金额:payRecordAmount 是完整实付(充电+保险),需扣掉保险。 + BigDecimal chargeOnlyAmount = null; + if (payRecordEvidence) { + BigDecimal insuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount()); + chargeOnlyAmount = payRecordAmount.subtract(insuranceAmount); + if (chargeOnlyAmount.compareTo(BigDecimal.ZERO) < 0) { + chargeOnlyAmount = BigDecimal.ZERO; + } + } return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(payRecordEvidence) .orderPaymentReady(false) - .paymentAmount(payRecordEvidence ? payRecordAmount : null) + .paymentAmount(chargeOnlyAmount) .paymentTime(paymentTime) .reason("订单主表支付字段不完整(payStatus=" + payStatus + ", payAmount=" + orderBasicInfo.getPayAmount() + ", payTime=" + orderBasicInfo.getPayTime() + ")") .build(); } - // payAmount 已通过回调/支付记录设置(微信/支付宝/白名单等),不应再加保险金额(避免双重计算) - if (payRecordEvidence && !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) { + // 兼容主表 payAmount 的两种历史口径,避免保险订单被误判为异常: + // - 旧口径:payAmount = 充电金额(历史/旧在线回调把保险减掉了),此时 payAmount + 保险 = 完整实付; + // - 新口径:payAmount = 完整金额(f11b781c2 起含保险),此时 payAmount 直接等于完整实付。 + // order_pay_record 汇总(payRecordAmount)始终是用户真实支付的完整金额(充电+保险), + // 故 payAmount==明细 或 payAmount+保险==明细 任一成立即视为一致;无保险时保险=0,两条等价。 + // (余额支付支路已在上方单独校验,此处仅覆盖非余额支付。) + BigDecimal insuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount()); + boolean amountConsistent = samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount) + || samePaymentAmount(orderBasicInfo.getPayAmount().add(insuranceAmount), payRecordAmount); + if (payRecordEvidence && !amountConsistent) { return OrderPaymentCheckResult.builder() .checkCompleted(true) .paymentEvidenceFound(true) .orderPaymentReady(false) .paymentTime(paymentTime) .reason("订单支付明细金额(" + payRecordAmount - + ")与订单主表支付金额(" + orderBasicInfo.getPayAmount() + ")不一致") + + ")与订单主表支付金额(" + orderBasicInfo.getPayAmount() + + ")+保险金额(" + insuranceAmount + ")均不一致") .build(); } return OrderPaymentCheckResult.builder() @@ -5406,12 +5431,8 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService { return; } - // 获取启动金额:保险金额不计入下发桩的充电额度 - BigDecimal payAmountForPile = dto.getPayAmount(); - if (dto.getInsuranceAmount() != null && dto.getInsuranceAmount().compareTo(BigDecimal.ZERO) > 0) { - payAmountForPile = dto.getPayAmount().subtract(dto.getInsuranceAmount()); - } - BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), payAmountForPile); + // 获取启动金额:dto.payAmount 已是充电金额(不含保险),直接用于下发桩 + BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), dto.getPayAmount()); // 发送启动指令 if (StringUtils.equals(pileConnectorDetailVO.getChargePortType(), Constants.THREE)) { diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java index 7f45776cc..e62c46a75 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java @@ -336,12 +336,20 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0"); } dto.setPayAmount(chargeAmount); + // 保险费:和在线支付一致,余额支付也需从钱包扣保险;无保险则为 0 + BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount() == null + ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount(); + if (insuranceAmount.compareTo(BigDecimal.ZERO) < 0) { + insuranceAmount = BigDecimal.ZERO; + } + // 实际应扣金额 = 充电金额 + 保险费(完整实付,写入 order_pay_record) + BigDecimal totalPayAmount = chargeAmount.add(insuranceAmount); // 总余额 BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null ? BigDecimal.ZERO : memberVO.getTotalBalance(); - if (totalAccountAmount.compareTo(chargeAmount) < 0) { - // 总余额小于充电金额 + if (totalAccountAmount.compareTo(totalPayAmount) < 0) { + // 总余额不足以覆盖 充电金额 + 保险费,直接拒绝 throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); } @@ -349,15 +357,16 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { BigDecimal principalBalancePay = BigDecimal.ZERO; // 赠送金支付金额 BigDecimal giftBalancePay = BigDecimal.ZERO; - // 计算下发金额 - Map stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount); + // 计算下发金额:按完整实付(充电+保险)拆分本金/赠金 + Map stringBigDecimalMap = calculateTheAmount(memberVO, totalPayAmount); if (stringBigDecimalMap != null) { principalBalancePay = stringBigDecimalMap.get("principalBalancePay"); giftBalancePay = stringBigDecimalMap.get("giftBalancePay"); } - // 更新支付金额 = 本金支付金额 + 赠送金支付金额,确保与 order_pay_record 汇总一致 - chargeAmount = principalBalancePay.add(giftBalancePay); + // 实际扣款合计(= 完整实付,含保险),用于扣款、order_pay_record、会员流水; + // 注意:主表 payAmount 仍只记充电金额(chargeAmount),不含保险。 + BigDecimal actualPaidAmount = principalBalancePay.add(giftBalancePay); // 更新会员钱包 全部金额都用于支付订单 UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder() @@ -421,7 +430,7 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic { .actionType(ActionTypeEnum.FORWARD.getValue()) .payMode(PayModeEnum.PAYMENT_OF_BALANCE.getValue()) .paymentInstitutions(PaymentInstitutionsEnum.LOCAL_ACCOUNTS.getValue()) - .amount(dto.getPayAmount()) // 单位元 + .amount(actualPaidAmount) // 单位元,完整实付(充电+保险) .build(); memberTransactionRecordService.insertSelective(record); } diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java index 5d6fd34e9..1c7d959bf 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java @@ -248,12 +248,20 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0"); } dto.setPayAmount(chargeAmount); + // 保险费:和在线支付一致,余额支付也需从钱包扣保险;无保险则为 0 + BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount() == null + ? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount(); + if (insuranceAmount.compareTo(BigDecimal.ZERO) < 0) { + insuranceAmount = BigDecimal.ZERO; + } + // 实际应扣金额 = 充电金额 + 保险费(完整实付,写入 order_pay_record) + BigDecimal totalPayAmount = chargeAmount.add(insuranceAmount); // 总余额 BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null ? BigDecimal.ZERO : memberVO.getTotalBalance(); - if (totalAccountAmount.compareTo(chargeAmount) < 0) { - // 总余额小于充电金额 + if (totalAccountAmount.compareTo(totalPayAmount) < 0) { + // 总余额不足以覆盖 充电金额 + 保险费,直接拒绝 throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT); } @@ -261,15 +269,16 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { BigDecimal principalBalancePay = BigDecimal.ZERO; // 赠送金支付金额 BigDecimal giftBalancePay = BigDecimal.ZERO; - // 计算下发金额 - Map stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount); + // 计算下发金额:按完整实付(充电+保险)拆分本金/赠金 + Map stringBigDecimalMap = calculateTheAmount(memberVO, totalPayAmount); if (stringBigDecimalMap != null) { principalBalancePay = stringBigDecimalMap.get("principalBalancePay"); giftBalancePay = stringBigDecimalMap.get("giftBalancePay"); } - // 更新支付金额 = 本金支付金额 + 赠送金支付金额 - chargeAmount = principalBalancePay.add(giftBalancePay); + // 实际扣款合计(= 完整实付,含保险),用于扣款、order_pay_record、会员流水; + // 注意:主表 payAmount 仍只记充电金额(chargeAmount),不含保险。 + BigDecimal actualPaidAmount = principalBalancePay.add(giftBalancePay); // 更新会员钱包 全部金额都用于支付订单 UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder() @@ -333,7 +342,7 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic { .actionType(ActionTypeEnum.FORWARD.getValue()) .payMode(PayModeEnum.PAYMENT_OF_BALANCE.getValue()) .paymentInstitutions(PaymentInstitutionsEnum.LOCAL_ACCOUNTS.getValue()) - .amount(dto.getPayAmount()) // 单位元 + .amount(actualPaidAmount) // 单位元,完整实付(充电+保险) .build(); memberTransactionRecordService.insertSelective(record); } From 808737c65e42ebd45fd01894dc7d57c8c49ac4ad Mon Sep 17 00:00:00 2001 From: Lemon Date: Tue, 4 Aug 2026 15:53:16 +0800 Subject: [PATCH 15/17] =?UTF-8?q?update=20=20=E5=90=8E=E7=AE=A1=E5=A4=A7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=B9=B3=E5=8F=B0=E7=9B=B4=E6=B5=81=E3=80=81?= =?UTF-8?q?=E4=BA=A4=E6=B5=81=E6=A1=A9=E6=95=B0=E9=87=8F=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=94=BE=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsowell/web/controller/index/indexController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java index ab3e1621e..1d0798b44 100644 --- a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java @@ -239,6 +239,11 @@ public class indexController extends BaseController { String totalChargingAmount = situation.getTotalChargingAmount(); String totalChargingDegree = situation.getTotalChargingDegree(); Long totalPileCount = totalPilesFuture.get(); + // 直流充电桩数量 + Long dcPileNums = dcPilesFuture.get(); + // 交流充电桩数量 + Long acPileNums = acPilesFuture.get(); + // 总订单数从settle_order_report的GROUP BY结果中获取(charge_num汇总) String totalOrderCountStr = situation.getTotalChargingQuantity(); Long totalOrderCount = (totalOrderCountStr != null && !totalOrderCountStr.isEmpty()) @@ -251,6 +256,8 @@ public class indexController extends BaseController { totalChargingDegree = new BigDecimal(totalChargingDegree).multiply(multiplier).toString(); totalPileCount = totalPileCount != null ? totalPileCount * 3 : 0L; totalOrderCount = totalOrderCount * 3; + dcPileNums = dcPileNums * 3; + acPileNums = acPileNums * 3; } overviewVO.setTotalUsers(totalUsersFuture.get()); @@ -268,8 +275,8 @@ public class indexController extends BaseController { overviewVO.setMonthlyAvgElectricity(monthlyAvg != null ? monthlyAvg.toPlainString() : "0"); overviewVO.setTotalGuns(totalGunsFuture.get()); overviewVO.setOnlinePiles(onlinePilesFuture.get()); - overviewVO.setOnlineStations(dcPilesFuture.get()); - overviewVO.setOnlineGuns(acPilesFuture.get()); + overviewVO.setOnlineStations(dcPileNums); + overviewVO.setOnlineGuns(acPileNums); // 节能减排计算 BigDecimal totalElecKwh = new BigDecimal(totalChargingDegree); From 231c9f513d6940ed713194af8fb0fc1cf286120e Mon Sep 17 00:00:00 2001 From: Lemon Date: Tue, 4 Aug 2026 15:58:47 +0800 Subject: [PATCH 16/17] update --- .../java/com/jsowell/web/controller/index/indexController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java index 1d0798b44..b990ac5bb 100644 --- a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java @@ -257,7 +257,7 @@ public class indexController extends BaseController { totalPileCount = totalPileCount != null ? totalPileCount * 3 : 0L; totalOrderCount = totalOrderCount * 3; dcPileNums = dcPileNums * 3; - acPileNums = acPileNums * 3; + acPileNums = totalPileCount - dcPileNums; } overviewVO.setTotalUsers(totalUsersFuture.get()); From f348ae9733c9060a48c0d934b6e9da32aed0cf86 Mon Sep 17 00:00:00 2001 From: Lemon Date: Tue, 4 Aug 2026 16:10:02 +0800 Subject: [PATCH 17/17] update --- .../com/jsowell/web/controller/index/indexController.java | 4 ++-- .../main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java index b990ac5bb..256ff4e75 100644 --- a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java @@ -275,8 +275,8 @@ public class indexController extends BaseController { overviewVO.setMonthlyAvgElectricity(monthlyAvg != null ? monthlyAvg.toPlainString() : "0"); overviewVO.setTotalGuns(totalGunsFuture.get()); overviewVO.setOnlinePiles(onlinePilesFuture.get()); - overviewVO.setOnlineStations(dcPileNums); - overviewVO.setOnlineGuns(acPileNums); + overviewVO.setDcPileNums(dcPileNums); + overviewVO.setAcPileNums(acPileNums); // 节能减排计算 BigDecimal totalElecKwh = new BigDecimal(totalChargingDegree); diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java index 50d385df2..351e71d9c 100644 --- a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java @@ -72,12 +72,12 @@ public class BigDataOverviewVO { /** * 直流充电桩数量 */ - private Long onlineStations; + private Long dcPileNums; /** * 交流充电桩数量 */ - private Long onlineGuns; + private Long acPileNums; /** * 累计碳减排量(吨)