ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUser.java
@@ -132,6 +132,10 @@ @TableField(exist = false) private String tagName; @ApiModelProperty(value = "是否白名单:1=是,2=否") @TableField(exist = false) private Integer whitelistFlag; @ApiModelProperty(value = "单位名称") @TableField(exist = false) private String companyName; ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -428,6 +428,31 @@ return R.ok(page); } List<Long> pageUserIds = page.getRecords().stream().map(TAppUser::getId).collect(Collectors.toList()); List<String> pageUserPhones = page.getRecords().stream() .map(TAppUser::getPhone) .filter(StringUtils::isNotBlank) .map(String::trim) .distinct() .collect(Collectors.toList()); LambdaQueryChainWrapper<TAppUserWhitelist> pageWhitelistQuery = appUserWhitelistService.lambdaQuery() .eq(TAppUserWhitelist::getStatus, 1) .eq(TAppUserWhitelist::getDelFlag, false) .and(wrapper -> { wrapper.in(TAppUserWhitelist::getAppUserId, pageUserIds); if (!pageUserPhones.isEmpty()) { wrapper.or().in(TAppUserWhitelist::getPhone, pageUserPhones); } }); List<TAppUserWhitelist> pageWhitelistList = pageWhitelistQuery.list(); Set<Long> activeWhitelistUserIds = pageWhitelistList.stream() .map(TAppUserWhitelist::getAppUserId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Set<String> activeWhitelistPhones = pageWhitelistList.stream() .map(TAppUserWhitelist::getPhone) .filter(StringUtils::isNotBlank) .map(String::trim) .collect(Collectors.toSet()); Map<Long, String> userTagNameMap = getUserTagNameMap(pageUserIds); Map<Long, BigDecimal> exchangeCodeBalanceMap = otherClient.getExchangeCodeBalanceMap(pageUserIds).getData(); if (exchangeCodeBalanceMap == null) { @@ -441,6 +466,9 @@ for (TAppUser appUser : page.getRecords()) { appUser.setExchangeCodeBalance(exchangeCodeBalanceMap.getOrDefault(appUser.getId(), BigDecimal.ZERO)); appUser.setTagName(userTagNameMap.get(appUser.getId())); boolean activeWhitelist = activeWhitelistUserIds.contains(appUser.getId()) || (StringUtils.isNotBlank(appUser.getPhone()) && activeWhitelistPhones.contains(appUser.getPhone().trim())); appUser.setWhitelistFlag(activeWhitelist ? 1 : 2); if (appUser.getVipEndTime()!=null&&appUser.getVipEndTime().isAfter(LocalDateTime.now())) { //匹配vipMap的值 appUser.setVipName(vipMap.getData().get(appUser.getVipId())); @@ -731,11 +759,7 @@ List<TAppUserCar> carList = appUserCarService.lambdaQuery().eq(TAppUserCar::getAppUserId, id).orderByDesc(BasePojo::getCreateTime).list(); userDetailDto.setCarList(carList); List<Integer> tagIds = appUserTagService.lambdaQuery().eq(TAppUserTag::getAppUserId, id).orderByDesc(TAppUserTag::getCreateTime).list().stream().map(TAppUserTag::getUserTagId).collect(Collectors.toList()); R<Map<Integer, String>> tagMap = otherClient.getTagMap(tagIds); userDetailDto.setTagName(tagMap.getData().values() .stream() .collect(Collectors.joining(","))); userDetailDto.setTagName(getUserTagNameMap(Collections.singletonList(id)).get(id)); ruoyi-service/ruoyi-chargingPile/src/main/resources/logback.xml
@@ -8,7 +8,7 @@ <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 --> <!-- <property name="log.path" value="/data/log/app/ruoyi-chargingPile"/>--> <property name="log.path" value="/Users/mitao/Documents/log/mx_charging_pile/log/app/ruoyi-chargingPile"/> <property name="log.path" value="/Users/PC/Documents/log/mx_charging_pile/log/app/ruoyi-chargingPile"/> <!--0. 日志格式和颜色渲染 --> <!-- 彩色日志依赖的渲染类 --> ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -440,14 +440,16 @@ @ResponseBody @PostMapping(value = "/pay/order/special/refund/query") @ApiOperation(value = "特殊退款查询", tags = {"管理后台-支付订单-订单信息"}) @RequiresPermissions(value = {"/paymentOrder/specialRefund", "/paymentOrder/specialRefund/refund"}, logical = Logical.OR) @ApiOperation(value = "特殊退款查询", tags = {"管理后台-支付订单-特殊退款处理"}) public R<PayOrderInfoDto> specialRefundQuery(@RequestBody PayOrderRefundDto payOrderQueryDto) { return chargingOrderService.getSpecialRefundOrder(payOrderQueryDto); } @ResponseBody @PostMapping(value = "/pay/order/special/refund") @ApiOperation(value = "特殊退款", tags = {"管理后台-支付订单-订单信息"}) @RequiresPermissions(value = {"/paymentOrder/specialRefund/refund"}, logical = Logical.OR) @ApiOperation(value = "特殊退款", tags = {"管理后台-支付订单-特殊退款处理"}) public R specialRefund(@RequestBody PayOrderRefundDto payOrderQueryDto) { return chargingOrderService.specialRefund(payOrderQueryDto); } @@ -616,7 +618,7 @@ @RequiresPermissions(value = {"/paymentOrder/refund", "/paymentOrder/order"}, logical = Logical.OR) @RequiresPermissions(value = {"/paymentOrder/refund", "/paymentOrder/order", "/paymentOrder/specialRefund"}, logical = Logical.OR) @ResponseBody @PostMapping(value = "/pay/order/refund/list") @ApiOperation(value = "列表", tags = {"管理后台-支付订单-退款订单"}) ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderRefundDto.java
@@ -16,7 +16,7 @@ private String code; @ApiModelProperty("支付流水号") private String serialNumber; @ApiModelProperty("支付宝特殊退款方式:ALI=支付宝退款,ICBC=工商退款") @ApiModelProperty("退款通道(兼容旧参数,特殊退款按原支付路径退款)") private String refundChannel; private BigDecimal refundAmount; private String reason; ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingBillServiceImpl.java
@@ -36,6 +36,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; @@ -43,6 +44,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** @@ -1057,10 +1060,10 @@ List<Map<String, Object>> list = chargingSettlementBillService.queryList(pageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds()); ChargingBillWapper chargingBillVO = new ChargingBillWapper(); List<Map<String, Object>> pageList = new ArrayList<>(); List<TChargingOrder> total = chargingOrderList.selectList(new LambdaQueryWrapper<>(TChargingOrder.class) .isNotNull(TChargingOrder::getPayTime)); List<TChargingOrderAccountingStrategy> strategyList = chargingOrderAccountingStrategyService.list(); Map<Integer, BigDecimal> siteServiceShareRatioMap = loadSiteServiceShareRatioMap(); PageInfo<Map<String, Object>> sumPageInfo = new PageInfo<>(1, 100000); List<Map<String, Object>> sumList = chargingSettlementBillService.queryList(sumPageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds()); Map<String, Map<String, BigDecimal>> incomeIndex = buildChargingBillIncomeIndex(sumList, siteServiceShareRatioMap); if(null != list){ for (Map<String, Object> map : list) { Integer site_id = Integer.valueOf(map.get("site_id").toString()); @@ -1077,7 +1080,7 @@ String chargingCapacity = map.get("total_electricity").toString(); String electrovalence = map.get("total_electricity_charge").toString(); String serviceCharge = map.get("total_service_fee").toString(); Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), site_id, billingCycle, total, strategyList, siteServiceShareRatioMap); Map<String, BigDecimal> incomeMap = getChargingBillIncome(incomeIndex, site_id, billingCycle); Map<String, Object> data1 = new HashMap<>(); data1.put("code", code); data1.put("orderState", 2); @@ -1118,13 +1121,11 @@ BigDecimal discount = (null == map || null == map.get("couponDeduction")) ? BigDecimal.ZERO : new BigDecimal(map.get("couponDeduction").toString()); BigDecimal platformIncome = BigDecimal.ZERO; BigDecimal partnerIncome = BigDecimal.ZERO; PageInfo<Map<String, Object>> sumPageInfo = new PageInfo<>(1, 100000); List<Map<String, Object>> sumList = chargingSettlementBillService.queryList(sumPageInfo, dto.getType(), dto.getState(), dto.getDateRange(), statisticalDateType, dto.getSiteIds()); if (sumList != null) { for (Map<String, Object> row : sumList) { Integer siteId = Integer.valueOf(row.get("site_id").toString()); String billingCycle = row.get("billingCycle").toString(); Map<String, BigDecimal> incomeMap = calculateChargingBillIncome(dto.getType(), siteId, billingCycle, total, strategyList, siteServiceShareRatioMap); Map<String, BigDecimal> incomeMap = getChargingBillIncome(incomeIndex, siteId, billingCycle); platformIncome = platformIncome.add(incomeMap.get("platformIncome")); partnerIncome = partnerIncome.add(incomeMap.get("partnerIncome")); } @@ -1145,6 +1146,111 @@ return chargingBillVO; } private Map<String, Map<String, BigDecimal>> buildChargingBillIncomeIndex(List<Map<String, Object>> billRows, Map<Integer, BigDecimal> siteServiceShareRatioMap) { Map<String, Map<String, BigDecimal>> incomeIndex = new HashMap<>(); if (billRows == null || billRows.isEmpty()) { return incomeIndex; } Set<String> billingCycles = billRows.stream() .map(row -> row.get("billingCycle")) .filter(Objects::nonNull) .map(Object::toString) .collect(Collectors.toSet()); if (billingCycles.isEmpty()) { return incomeIndex; } LocalDate firstMonth = billingCycles.stream() .map(cycle -> LocalDate.parse(cycle + "-01")) .min(LocalDate::compareTo) .orElse(null); LocalDate lastMonth = billingCycles.stream() .map(cycle -> LocalDate.parse(cycle + "-01")) .max(LocalDate::compareTo) .orElse(null); if (firstMonth == null || lastMonth == null) { return incomeIndex; } List<TChargingOrder> orders = chargingOrderList.selectList(new LambdaQueryWrapper<>(TChargingOrder.class) .select(TChargingOrder::getId, TChargingOrder::getSiteId, TChargingOrder::getPayTime, TChargingOrder::getOrderSource) .ge(TChargingOrder::getPayTime, firstMonth.atStartOfDay()) .lt(TChargingOrder::getPayTime, lastMonth.plusMonths(1).atStartOfDay())); if (orders == null || orders.isEmpty()) { return incomeIndex; } Map<Long, BigDecimal[]> strategyAmountMap = new HashMap<>(); List<Long> orderIds = orders.stream().map(TChargingOrder::getId).filter(Objects::nonNull).collect(Collectors.toList()); final int batchSize = 1000; for (int from = 0; from < orderIds.size(); from += batchSize) { List<Long> batchIds = orderIds.subList(from, Math.min(from + batchSize, orderIds.size())); List<TChargingOrderAccountingStrategy> strategies = chargingOrderAccountingStrategyService.list( new LambdaQueryWrapper<TChargingOrderAccountingStrategy>() .select(TChargingOrderAccountingStrategy::getChargingOrderId, TChargingOrderAccountingStrategy::getPeriodElectricPrice, TChargingOrderAccountingStrategy::getPeriodServicePrice) .in(TChargingOrderAccountingStrategy::getChargingOrderId, batchIds)); for (TChargingOrderAccountingStrategy strategy : strategies) { BigDecimal[] amounts = strategyAmountMap.computeIfAbsent(strategy.getChargingOrderId(), key -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO}); amounts[0] = amounts[0].add(strategy.getPeriodElectricPrice() == null ? BigDecimal.ZERO : strategy.getPeriodElectricPrice()); amounts[1] = amounts[1].add(strategy.getPeriodServicePrice() == null ? BigDecimal.ZERO : strategy.getPeriodServicePrice()); } } for (TChargingOrder order : orders) { if (order.getPayTime() == null || order.getSiteId() == null) { continue; } String billingCycle = order.getPayTime().format(DateTimeFormatter.ofPattern("yyyy-MM")); if (!billingCycles.contains(billingCycle)) { continue; } BigDecimal[] amounts = strategyAmountMap.getOrDefault(order.getId(), new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO}); BigDecimal paymentAmount; BigDecimal partnerIncome = BigDecimal.ZERO; if (Integer.valueOf(2).equals(order.getOrderSource())) { paymentAmount = amounts[0].add(amounts[1].multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.DOWN)); } else { paymentAmount = amounts[0].add(amounts[1]); if (Integer.valueOf(1).equals(order.getOrderSource())) { BigDecimal serviceShareRatio = siteServiceShareRatioMap.get(order.getSiteId()); if (serviceShareRatio != null && serviceShareRatio.compareTo(BigDecimal.ZERO) > 0) { partnerIncome = amounts[1].multiply(serviceShareRatio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } } } addChargingBillIncome(incomeIndex, chargingBillIncomeKey(0, billingCycle), paymentAmount, partnerIncome); addChargingBillIncome(incomeIndex, chargingBillIncomeKey(order.getSiteId(), billingCycle), paymentAmount, partnerIncome); } return incomeIndex; } private void addChargingBillIncome(Map<String, Map<String, BigDecimal>> incomeIndex, String key, BigDecimal paymentAmount, BigDecimal partnerIncome) { Map<String, BigDecimal> income = incomeIndex.computeIfAbsent(key, ignored -> { Map<String, BigDecimal> value = new HashMap<>(); value.put("platformIncome", BigDecimal.ZERO); value.put("partnerIncome", BigDecimal.ZERO); return value; }); income.put("platformIncome", income.get("platformIncome").add(paymentAmount).subtract(partnerIncome)); income.put("partnerIncome", income.get("partnerIncome").add(partnerIncome)); } private Map<String, BigDecimal> getChargingBillIncome(Map<String, Map<String, BigDecimal>> incomeIndex, Integer siteId, String billingCycle) { Map<String, BigDecimal> income = incomeIndex.get(chargingBillIncomeKey(siteId, billingCycle)); Map<String, BigDecimal> result = new HashMap<>(); result.put("platformIncome", income == null ? BigDecimal.ZERO : income.get("platformIncome").max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP)); result.put("partnerIncome", income == null ? BigDecimal.ZERO : income.get("partnerIncome").setScale(2, RoundingMode.HALF_UP)); return result; } private String chargingBillIncomeKey(Integer siteId, String billingCycle) { return (siteId == null ? 0 : siteId) + "|" + billingCycle; } @Override public ChargingBillVO chargingBillListExport(ChargingListQuery dto) { if (dto.getState()!=null){ ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -966,7 +966,13 @@ req.setReturnUrl(addChargingOrder.getReturnUrl()); req.setOpenId(appUser.getWxOpenid()); req.setProfitSharing(buildIcbcProfitSharing(site)); IcbcPaymentResp data = icbcPaymentClient.payment(req).getData(); R<IcbcPaymentResp> result = icbcPaymentClient.payment(req); if (result == null || R.isError(result)) { String reason = result == null ? "工行支付服务无响应" : firstNotBlank(result.getMsg(), "工行支付服务调用失败"); log.error("调起工行支付失败,订单号:{},原因:{}", chargingOrder.getCode(), reason); throw new RuntimeException("调起工行支付失败:" + reason); } IcbcPaymentResp data = result.getData(); if (isIcbcPaymentCreated(data)) { if (StringUtils.hasLength(data.getTradeNo())) { chargingOrder.setRechargeSerialNumber(data.getTradeNo()); @@ -974,7 +980,9 @@ } return AjaxResult.success(data); } throw new RuntimeException("调起工行支付失败"); String reason = buildIcbcPaymentFailReason(data); log.error("调起工行支付失败,订单号:{},工行返回:{}", chargingOrder.getCode(), reason); throw new RuntimeException("调起工行支付失败:" + reason); } if (Integer.valueOf(2).equals(addChargingOrder.getPaymentType())) { AliPaymentReq req = new AliPaymentReq(); @@ -2385,12 +2393,23 @@ * @param chargingOrder */ public void editUserTag(TChargingOrder chargingOrder) { if (chargingOrder == null || chargingOrder.getAppUserId() == null) { return; } //处理用户标签数据 List<TUserTag> data = userTagClient.getAllUserTag().getData(); R<List<TUserTag>> tagResult = userTagClient.getAllUserTag(); List<TUserTag> data = tagResult == null ? null : tagResult.getData(); if (data == null || data.isEmpty()) { return; } //累计充电次数 List<TUserTag> userTagList1 = data.stream().filter(s -> s.getStandardCondition() == 1).collect(Collectors.toList()); List<TUserTag> userTagList1 = data.stream().filter(s -> Integer.valueOf(1).equals(s.getStandardCondition())).collect(Collectors.toList()); for (TUserTag tUserTag : userTagList1) { Integer times = JSON.parseObject(tUserTag.getConditions()).getInteger("times"); JSONObject conditions = JSON.parseObject(tUserTag.getConditions()); Integer times = conditions == null ? null : conditions.getInteger("times"); if (times == null || times <= 0 || tUserTag.getCreateTime() == null) { continue; } TAppUserTag appUserTag = new TAppUserTag(); appUserTag.setAppUserId(chargingOrder.getAppUserId()); appUserTag.setUserTagId(tUserTag.getId()); @@ -2409,18 +2428,21 @@ } } //充电频率 List<TUserTag> userTagList2 = data.stream().filter(s -> s.getStandardCondition() == 2).collect(Collectors.toList()); List<TUserTag> userTagList2 = data.stream().filter(s -> Integer.valueOf(2).equals(s.getStandardCondition())).collect(Collectors.toList()); for (TUserTag tUserTag : userTagList2) { TAppUserTag appUserTag = new TAppUserTag(); appUserTag.setAppUserId(chargingOrder.getAppUserId()); appUserTag.setUserTagId(tUserTag.getId()); TAppUserTag data1 = appUserTagClient.getUserTag(appUserTag).getData(); Integer day = JSON.parseObject(tUserTag.getConditions()).getInteger("day"); Integer times = JSON.parseObject(tUserTag.getConditions()).getInteger("times"); JSONObject conditions = JSON.parseObject(tUserTag.getConditions()); Integer day = conditions == null ? null : conditions.getInteger("day"); Integer times = conditions == null ? null : conditions.getInteger("times"); if (day == null || day <= 0 || times == null || times <= 0) { continue; } LocalDateTime endTime = LocalDateTime.now(); endTime.plusDays(day); String start = tUserTag.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String start = endTime.minusDays(day).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String end = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); long count1 = this.count(new LambdaQueryWrapper<TChargingOrder>().eq(TChargingOrder::getAppUserId, chargingOrder.getAppUserId()) .eq(TChargingOrder::getRechargePaymentStatus, 2).between(TChargingOrder::getStartTime, start, end).eq(TChargingOrder::getDelFlag, 0)); @@ -2934,13 +2956,23 @@ @Override public R<PageInfo<PayOrderDto>> payOrderQuery(PayOrderQueryDto payOrderQueryDto) { if (payOrderQueryDto == null) { payOrderQueryDto = new PayOrderQueryDto(); } PageInfo<PayOrderDto> pageInfo = new PageInfo<>(payOrderQueryDto.getPageCurr(), payOrderQueryDto.getPageSize()); List<PayOrderDto> list = this.baseMapper.payOrderQuery(pageInfo, payOrderQueryDto); if (list == null || list.isEmpty()) { pageInfo.setRecords(Collections.emptyList()); return R.ok(pageInfo); } for (PayOrderDto payOrderDto : list) { if (payOrderDto.getType() == 2 || payOrderDto.getType() == 3) { if (payOrderDto == null) { continue; } if (Integer.valueOf(2).equals(payOrderDto.getType()) || Integer.valueOf(3).equals(payOrderDto.getType())) { payOrderDto.setFinalAmount(payOrderDto.getOrderAmount()); } if (payOrderDto.getType() == 1 && payOrderDto.getStatus() == 3) { if (Integer.valueOf(1).equals(payOrderDto.getType()) && Integer.valueOf(3).equals(payOrderDto.getStatus())) { payOrderDto.setFinalAmount(payOrderDto.getOrderAmount()); payOrderDto.setOrderAmount(payOrderDto.getOrderAmount()); payOrderDto.setPaymentAmount(payOrderDto.getOrderAmount()); @@ -4700,61 +4732,189 @@ if (order == null) { return R.fail("未查询到支付成功的充电订单"); } BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount()); BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP); if (refundAmount.compareTo(refundableAmount) > 0) { return R.fail("退款金额不能超过剩余可退金额"); RLock lock = redissonClient.getLock("SPECIAL_REFUND_ORDER_LOCK:" + order.getId()); if (!lock.tryLock()) { return R.fail("该订单正在退款处理中,请勿重复提交"); } BigDecimal refundedThirdAmount = money(order.getThirdRefundAmount()); BigDecimal refundableThirdAmount = money(order.getThirdPayAmount()).subtract(refundedThirdAmount); if (refundableThirdAmount.compareTo(BigDecimal.ZERO) < 0) { refundableThirdAmount = BigDecimal.ZERO; try { order = this.getById(order.getId()); if (order == null) { return R.fail("充电订单不存在"); } BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount()); BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP); if (refundAmount.compareTo(refundableAmount) > 0) { return R.fail("退款金额不能超过剩余可退金额"); } BigDecimal refundedThirdAmount = money(order.getThirdRefundAmount()); BigDecimal refundableThirdAmount = money(order.getThirdPayAmount()).subtract(refundedThirdAmount); if (refundableThirdAmount.compareTo(BigDecimal.ZERO) < 0) { refundableThirdAmount = BigDecimal.ZERO; } BigDecimal thirdRefundAmount = isBalancePayMode(order.getPayMethod()) ? refundAmount.min(refundableThirdAmount).setScale(2, RoundingMode.HALF_UP) : refundAmount; BigDecimal balanceRefundAmount = refundAmount.subtract(thirdRefundAmount).setScale(2, RoundingMode.HALF_UP); if (balanceRefundAmount.compareTo(BigDecimal.ZERO) < 0) { balanceRefundAmount = BigDecimal.ZERO; } TChargingOrderRefund refund = buildSpecialChargingRefund(order, payOrderQueryDto, refundAmount, balanceRefundAmount, thirdRefundAmount); if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) { refund.setRefundSerialNumber("BALANCE"); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); return R.ok(); } Integer payType = order.getRechargePaymentType(); boolean success = false; if (Integer.valueOf(1).equals(payType)) { R shareRefund = refundIcbcShareBeforeMainRefund(order, refund, thirdRefundAmount); if (shareRefund.getCode() != 200) { return shareRefund; } success = refundByIcbcAndFinish(order, refund, thirdRefundAmount, SPECIAL_REFUND_TITLE, true, refundAmount, balanceRefundAmount); } else if (Integer.valueOf(2).equals(payType)) { RefundReq dto = new RefundReq(); dto.setOutTradeNo(order.getCode()); dto.setOutRequestNo(refund.getRefundCode()); dto.setRefundAmount(thirdRefundAmount.toString()); dto.setRefundReason(SPECIAL_REFUND_TITLE); RefundResp resp = aliPaymentClient.refund(dto).getData(); if (resp != null) { refund.setRefundSerialNumber(resp.getTradeNo()); refund.setRefundTime(LocalDateTime.now()); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); success = true; } } else if (Integer.valueOf(3).equals(payType)) { RefundReq dto = new RefundReq(); dto.setOutTradeNo(order.getRechargeSerialNumber()); dto.setOutRequestNo(refund.getRefundCode()); dto.setRefundAmount(thirdRefundAmount.toString()); dto.setRefundReason(SPECIAL_REFUND_TITLE); RefundResp resp = h5AliPaymentClient.refund(dto).getData(); if (resp != null && "10000".equals(resp.getCode())) { refund.setRefundSerialNumber(resp.getTradeNo()); refund.setRefundTime(LocalDateTime.now()); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); success = true; } } return success ? R.ok() : R.fail("退款失败,请检查支付渠道或稍后重试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } BigDecimal thirdRefundAmount = isBalancePayMode(order.getPayMethod()) ? refundAmount.min(refundableThirdAmount).setScale(2, RoundingMode.HALF_UP) : refundAmount; BigDecimal balanceRefundAmount = refundAmount.subtract(thirdRefundAmount).setScale(2, RoundingMode.HALF_UP); if (balanceRefundAmount.compareTo(BigDecimal.ZERO) < 0) { balanceRefundAmount = BigDecimal.ZERO; } TChargingOrderRefund refund = buildSpecialChargingRefund(order, payOrderQueryDto, refundAmount, balanceRefundAmount, thirdRefundAmount); if (thirdRefundAmount.compareTo(BigDecimal.ZERO) == 0) { refund.setRefundSerialNumber("BALANCE"); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); } private R refundIcbcShareBeforeMainRefund(TChargingOrder order, TChargingOrderRefund refund, BigDecimal thirdRefundAmount) { if (order == null || refund == null || thirdRefundAmount == null || thirdRefundAmount.compareTo(BigDecimal.ZERO) <= 0) { return R.ok(); } Integer payType = order.getRechargePaymentType(); boolean success = false; if (Integer.valueOf(1).equals(payType) || "ICBC".equalsIgnoreCase(payOrderQueryDto.getRefundChannel())) { success = refundByIcbcAndFinish(order, refund, thirdRefundAmount, SPECIAL_REFUND_TITLE, true, refundAmount, balanceRefundAmount); } else if (Integer.valueOf(2).equals(payType)) { RefundReq dto = new RefundReq(); dto.setOutTradeNo(order.getCode()); dto.setOutRequestNo(refund.getRefundCode()); dto.setRefundAmount(thirdRefundAmount.toString()); dto.setRefundReason(SPECIAL_REFUND_TITLE); RefundResp resp = aliPaymentClient.refund(dto).getData(); if (resp != null) { refund.setRefundSerialNumber(resp.getTradeNo()); refund.setRefundTime(LocalDateTime.now()); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); success = true; if (!"ICBC".equals(order.getSourceName())) { return R.ok(); } List<TChargingOrderShareRecord> shareRecords = chargingOrderShareRecordMapper.selectList( new LambdaQueryWrapper<TChargingOrderShareRecord>() .eq(TChargingOrderShareRecord::getChargingOrderId, order.getId()) .eq(TChargingOrderShareRecord::getPayChannel, "ICBC") .eq(TChargingOrderShareRecord::getNeedOnlineShare, true) .eq(TChargingOrderShareRecord::getOnlineShareStatus, "SUCCESS") .eq(TChargingOrderShareRecord::getDelFlag, false) .orderByAsc(TChargingOrderShareRecord::getId)); if (shareRecords == null || shareRecords.isEmpty()) { return R.ok(); } BigDecimal thirdPayAmount = getRefundableThirdPayAmount(order); if (thirdPayAmount.compareTo(BigDecimal.ZERO) <= 0) { return R.fail("订单已分账但第三方支付金额异常,禁止退款"); } BigDecimal cumulativeThirdRefund = money(order.getThirdRefundAmount()).add(thirdRefundAmount).min(thirdPayAmount).setScale(2, RoundingMode.HALF_UP); for (TChargingOrderShareRecord shareRecord : shareRecords) { BigDecimal shareAmount = money(shareRecord.getShareAmount()); if (shareAmount.compareTo(BigDecimal.ZERO) <= 0) { continue; } } else if (Integer.valueOf(3).equals(payType)) { RefundReq dto = new RefundReq(); dto.setOutTradeNo(order.getRechargeSerialNumber()); dto.setOutRequestNo(refund.getRefundCode()); dto.setRefundAmount(thirdRefundAmount.toString()); dto.setRefundReason(SPECIAL_REFUND_TITLE); RefundResp resp = h5AliPaymentClient.refund(dto).getData(); if (resp != null && "10000".equals(resp.getCode())) { refund.setRefundSerialNumber(resp.getTradeNo()); refund.setRefundTime(LocalDateTime.now()); finishManualChargingRefund(order, refund, refundAmount, balanceRefundAmount, thirdRefundAmount); success = true; BigDecimal targetRefundAmount = shareAmount.multiply(cumulativeThirdRefund).divide(thirdPayAmount, 2, RoundingMode.HALF_UP).min(shareAmount).setScale(2, RoundingMode.HALF_UP); BigDecimal refundedShareAmount; try { refundedShareAmount = queryIcbcShareRefundAmount(order, shareRecord); } catch (ServiceException e) { return R.fail(e.getMessage()); } BigDecimal shareRefundAmount = targetRefundAmount.subtract(refundedShareAmount).setScale(2, RoundingMode.HALF_UP); if (shareRefundAmount.compareTo(BigDecimal.ZERO) <= 0) { continue; } R result = refundIcbcShareRecord(order, shareRecord, refund, shareRefundAmount); if (result.getCode() != 200) { return result; } } return success ? R.ok() : R.fail("退款失败,请检查支付渠道或稍后重试"); return R.ok(); } private BigDecimal queryIcbcShareRefundAmount(TChargingOrder order, TChargingOrderShareRecord shareRecord) { try { IcbcSubOrderQueryReq queryReq = IcbcAdminFundOperationSupport.buildSubOrderQueryRequest(order, shareRecord); R<IcbcSubOrderQueryResp> queryResult = icbcPaymentClient.querySubOrder(queryReq); IcbcSubOrderQueryResp queryResp = queryResult == null ? null : queryResult.getData(); if (queryResult == null || queryResult.getCode() != 200 || queryResp == null || !"SUCCESS".equals(queryResp.getStatus())) { throw new ServiceException(queryResult == null ? "工行子订单查询无响应" : firstNotBlank(queryResult.getMsg(), "工行子订单查询失败")); } return parseIcbcAmount(queryResp.getOrderRefundAmnt()); } catch (IllegalArgumentException e) { throw new ServiceException(e.getMessage()); } catch (ServiceException e) { throw e; } catch (Exception e) { throw new ServiceException("工行子订单查询异常:" + firstNotBlank(e.getMessage(), "未知异常")); } } private R refundIcbcShareRecord(TChargingOrder order, TChargingOrderShareRecord shareRecord, TChargingOrderRefund refund, BigDecimal shareRefundAmount) { String suffix = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String retSubOrderId = ("SPSUB" + refund.getRefundCode() + shareRecord.getId()).substring(0, Math.min(50, ("SPSUB" + refund.getRefundCode() + shareRecord.getId()).length())); String outtrxSerialNo = ("SPSUBREF" + suffix + shareRecord.getId()).substring(0, Math.min(50, ("SPSUBREF" + suffix + shareRecord.getId()).length())); try { IcbcSubOrderRefundReq refundReq = IcbcAdminFundOperationSupport.buildSubOrderRefundRequest( order, shareRecord, shareRefundAmount, retSubOrderId, outtrxSerialNo); refundReq.setMerSpInfor("特殊退款分账退回:" + order.getCode()); R<IcbcSubOrderRefundResp> refundResult = icbcPaymentClient.refundSubOrder(refundReq); IcbcSubOrderRefundResp refundResp = refundResult == null ? null : refundResult.getData(); if (refundResult == null || refundResult.getCode() != 200 || refundResp == null || !"SUCCESS".equals(refundResp.getStatus())) { String message = refundResult == null ? "工行分账退款无响应" : firstNotBlank(refundResult.getMsg(), refundResp == null ? null : refundResp.getRawResponse(), "工行分账退款失败"); log.error("特殊退款分账退回失败,订单号:{},退款单号:{},分账记录:{},金额:{},原因:{}", order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, message); return R.fail("分账退回失败:" + message); } log.info("特殊退款分账退回成功,订单号:{},退款单号:{},分账记录:{},金额:{}", order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount); return R.ok(); } catch (IllegalArgumentException | ServiceException e) { log.error("特殊退款分账退回失败,订单号:{},退款单号:{},分账记录:{},金额:{}", order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, e); return R.fail("分账退回失败:" + e.getMessage()); } catch (Exception e) { log.error("特殊退款分账退回异常,订单号:{},退款单号:{},分账记录:{},金额:{}", order.getCode(), refund.getRefundCode(), shareRecord.getId(), shareRefundAmount, e); return R.fail("分账退回异常:" + firstNotBlank(e.getMessage(), "未知异常")); } } private BigDecimal parseIcbcAmount(String amount) { if (!StringUtils.hasLength(amount)) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } String value = amount.trim(); try { if (value.contains(".")) { return money(new BigDecimal(value)); } return new BigDecimal(value).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } catch (NumberFormatException e) { throw new ServiceException("工行金额格式异常:" + amount); } } private R validateOperationPassword(PayOrderRefundDto payOrderQueryDto) { @@ -7038,6 +7198,20 @@ } } private String buildIcbcPaymentFailReason(IcbcPaymentResp resp) { if (resp == null) { return "工行支付服务未返回支付数据"; } String reason = firstNotBlank(resp.getRawResponse(), resp.getStatus()); if (!StringUtils.hasLength(reason)) { return "工行支付服务返回数据不完整"; } if (reason.length() > 500) { return reason.substring(0, 500); } return reason; } private void triggerChargingOrderShare(TChargingOrder chargingOrder, Site site) { if (!isNeedChargingOrderShare(chargingOrder)) { return; ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TUserTagController.java
@@ -14,6 +14,7 @@ import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -59,8 +60,11 @@ //获取标签map @PostMapping(value = "/getTagMap") public R<Map<Integer, String>> getTagMap(@RequestBody List<Integer> ids) { if (ids == null || ids.isEmpty()) { return R.ok(new HashMap<>()); } List<TUserTag> list = tUserTagService.lambdaQuery().in(!ids.isEmpty(), TUserTag::getId, ids).list(); List<TUserTag> list = tUserTagService.lambdaQuery().in(TUserTag::getId, ids).list(); Map<Integer, String> idNameMap = list.stream() .collect(Collectors.toMap( TUserTag::getId, ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
@@ -13,18 +13,20 @@ @Configuration @ConfigurationProperties(prefix = "icbc.pay") public class IcbcPayProperties { private static final String ICBC_GATEWAY = "https://gw.open.icbc.com.cn/api/cardbusiness"; private String appId; private String privateKey; private String icbcPublicKey; private String signType = "RSA2"; private String charset = "UTF-8"; private String format = "json"; private String gatewayUrl; private String orderQueryUrl; private String refundUrl; private String subOrderSplitUrl; private String subOrderRefundUrl; private String subOrderQueryUrl; private String gatewayUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/consumepurchase/V2"; private String orderQueryUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/orderqry/V2"; private String refundUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/merrefund/V1"; private String subOrderSplitUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/sersuborder/V1"; private String subOrderRefundUrl = ICBC_GATEWAY + "/aggregatepay/b2c/online/sersuborderrefund/V1"; private String subOrderQueryUrl = ICBC_GATEWAY + "/order/suborderquery/V1"; private String notifyUrl; private String merId; private String merPrtclNo; sql/20260720_special_refund_menu.sql
New file @@ -0,0 +1,60 @@ -- 支付订单:特殊退款处理菜单及执行按钮权限 SET @payment_order_id := ( SELECT menu_id FROM sys_menu WHERE menu_name = '支付订单' AND (perms = '/paymentOrder' OR path = '/order/payMentOrder' OR path = 'payMentOrder') ORDER BY menu_id ASC LIMIT 1 ); INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) SELECT '特殊退款处理', @payment_order_id, 3, '/order/payMentOrder/specialRefund', 'order/payMentOrder/specialRefund', 1, 0, 'C', '0', 0, '/paymentOrder/specialRefund', '#', 'admin', NOW(), '支付订单特殊退款处理' WHERE @payment_order_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM sys_menu WHERE parent_id = @payment_order_id AND (perms = '/paymentOrder/specialRefund' OR path = '/order/payMentOrder/specialRefund') ); SET @special_refund_id := ( SELECT menu_id FROM sys_menu WHERE parent_id = @payment_order_id AND (perms = '/paymentOrder/specialRefund' OR path = '/order/payMentOrder/specialRefund') ORDER BY menu_id ASC LIMIT 1 ); UPDATE sys_menu SET menu_name = '特殊退款处理', parent_id = @payment_order_id, order_num = 3, path = '/order/payMentOrder/specialRefund', component = 'order/payMentOrder/specialRefund', menu_type = 'C', visible = '0', status = 0, perms = '/paymentOrder/specialRefund', update_time = NOW() WHERE @special_refund_id IS NOT NULL AND menu_id = @special_refund_id; INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) SELECT '发起特殊退款', @special_refund_id, 1, '', NULL, 1, 0, 'F', '0', 0, '/paymentOrder/specialRefund/refund', '#', 'admin', NOW(), '发起特殊退款按钮权限' WHERE @special_refund_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM sys_menu WHERE parent_id = @special_refund_id AND perms = '/paymentOrder/specialRefund/refund' ); -- 管理员角色补上新权限;其他角色请在角色管理中按需勾选。 INSERT IGNORE INTO sys_role_menu (role_id, menu_id) SELECT 1, menu_id FROM sys_menu WHERE menu_id IN (@special_refund_id) OR parent_id = @special_refund_id;