修复余额不足仍可启动充电问题

统一启动金额优先级并在启动前校验会员余额。

同步完成订单创建和余额支付后再返回鉴权成功,并增加支付凭证补偿、异常停机和结算保护。
This commit is contained in:
Guoqs
2026-07-25 09:54:37 +08:00
parent bfa209becb
commit 1b37a7d5e8
18 changed files with 851 additions and 126 deletions

View File

@@ -264,6 +264,12 @@ public interface OrderBasicInfoMapper {
@Param("orderStatus") String orderStatus,
@Param("payStatus") String payStatus);
/**
* 查询需要进行支付凭证补偿的余额支付订单。
*/
List<OrderBasicInfo> queryOrdersForPaymentReconcile(@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 个人桩查询充电数据
*

View File

@@ -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<OrderVO> getListByOrderCodes(List<String> orderCodes);
/**

View File

@@ -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 {
}
}
/**
* 校验订单支付凭证,并在支付流水完整、但订单主表字段尚未落库时进行修复。
*
* <p>余额支付不能只相信 order_basic_info.pay_status。必须同时存在订单支付明细
* 和会员钱包出账流水,且两者金额与主表支付金额一致,才允许进入充电或结算流程。</p>
*/
@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<OrderBasicInfo> 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<OrderPayRecord> payRecords;
List<MemberWalletLog> 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<MemberWalletLog> logs = memberWalletLogService.getOrderRecord(
orderBasicInfo.getOrderCode(), MemberWalletEnum.TYPE_OUT.getValue());
walletLogs = logs == null ? Collections.<MemberWalletLog>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<String, Object> resultMap = generateOrderByCardV2(generateOrderDTO);
} catch (Exception e) {
throw new RuntimeException(e);
try {
Map<String, Object> 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;
}

View File

@@ -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中的总用电量

View File

@@ -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);

View File

@@ -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<String, BigDecimal> stringBigDecimalMap = calculateTheAmount(memberVO);
Map<String, BigDecimal> 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);

View File

@@ -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;
}
}

View File

@@ -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;
/**
* 订单支付凭证校验结果。
*
* <p>paymentEvidenceFound 表示底层支付流水是否真实存在;
* orderPaymentReady 表示订单主表字段是否已经可以安全参与后续业务。</p>
*/
@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;
}