Merge branch 'dev' of codeup.aliyun.com:67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web into dev

# Conflicts:
#	jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java
#	jsowell-pile/src/main/java/com/jsowell/pile/vo/web/BigDataOverviewVO.java
This commit is contained in:
jsowell
2026-08-04 16:33:03 +08:00
32 changed files with 2140 additions and 146 deletions

View File

@@ -11,9 +11,12 @@ import com.google.common.collect.ImmutableMap;
import com.jsowell.pile.dto.IndexQueryDTO;
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.service.OrderBasicInfoService;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO;
@@ -21,6 +24,7 @@ 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.TimeDistributionVO;
import org.apache.commons.lang3.StringUtils;
@@ -47,6 +51,59 @@ public class BusinessFinancialController extends BaseController {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
/**
* 查询工作台首页汇总
*
* @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;
}
/**
* 我的钱包界面查询接口
*

View File

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

View File

@@ -1299,19 +1299,20 @@ public class OrderService {
}
}, executor);
// 初始化充电金额
BigDecimal chargeAmount = amount;
// 支付订单成功
OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount();
if (insuranceAmount.compareTo(BigDecimal.ZERO) > 0) {
// 如果用户支付了保险金额,则充电金额需将保险金额减去
// 主表 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(chargeAmount) // amount
// .payMode(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue())
.payAmount(chargeAmount) // 充电金额(不含保险)
.payMode(payModel)
.acquirer(AcquirerEnum.ADAPAY.getValue())
.build();

View File

@@ -272,6 +272,7 @@ public class indexController extends BaseController {
String totalChargingAmount = situation.getTotalChargingAmount();
String totalChargingDegree = situation.getTotalChargingDegree();
Long totalPileCount = totalPilesFuture.get();
// 总订单数从settle_order_report的GROUP BY结果中获取charge_num汇总
String totalOrderCountStr = situation.getTotalChargingQuantity();
Long totalOrderCount = (totalOrderCountStr != null && !totalOrderCountStr.isEmpty())

View File

@@ -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 依赖,不连库)。
*
* <p>规则:主表 payAmount = 充电金额order_pay_record 汇总 = 完整实付(充电 + 保险)
* {@code payAmount==明细 或 payAmount+保险==明细} 任一成立即视为一致。</p>
*/
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<OrderPayRecord> records, List<MemberWalletLog> 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());
}
}

View File

@@ -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;
/**
* 余额支付「电费 + 保险费」退款计算的单元测试(纯计算,不连库)。
*
* <p>前提order_pay_record 汇总(principalPay + giftPay) = 完整实付(充电 + 保险)
* 故 {@code 电费预付款 = 总支付 - 保险}。这是「余额与在线一样从钱包扣保险」之后才成立的口径。</p>
*
* <p>覆盖场景:</p>
* <ol>
* <li>无保险 + 正常消费(回归,行为不变)</li>
* <li>有保险 + 正常消费(消费≥1) → 保险不退</li>
* <li>有保险 + 消费&lt;1 → 保险退</li>
* <li>保险跨本金/赠金拆分 + 消费&lt;1</li>
* <li>折扣把实际电费消费压到&lt;1 → 保险退</li>
* <li>消费恰好=1边界→ 保险不退</li>
* </ol>
*/
class BalanceInsuranceRefundTest {
private static Map<String, BigDecimal> 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<String, BigDecimal> result = (Map<String, BigDecimal>) 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<String, BigDecimal> 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");
}
}

View File

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