longz
2026-07-28 cd4e26608b01cbc376760d96e2eab5d0a796f0b0
合并监控
3个文件已添加
14个文件已修改
423 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java
@@ -2,6 +2,7 @@
import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.common.core.domain.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +36,11 @@
            public R<Partner> getPartnerByAppUserId(Long appUserId) {
                return R.fail("根据小程序用户获取合作商信息失败:" + throwable.getMessage());
            }
            @Override
            public R<List<TPartnerAppUser>> getBoundAppUserRelations(Integer partnerId) {
                return R.fail("获取合作商绑定用户失败:" + throwable.getMessage());
            }
            
            @Override
            public R<Set<Integer>> authSite(Integer partnerId, String siteMenu) {
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java
@@ -2,6 +2,7 @@
import com.ruoyi.chargingPile.api.factory.PartnerFallbackFactory;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.domain.AjaxResult;
@@ -31,6 +32,9 @@
    @PostMapping("/partner/getPartnerByAppUserId/{appUserId}")
    R<Partner> getPartnerByAppUserId(@PathVariable("appUserId") Long appUserId);
    @PostMapping("/partner/appUser/relations/{partnerId}")
    R<List<TPartnerAppUser>> getBoundAppUserRelations(@PathVariable("partnerId") Integer partnerId);
    
    
    /**
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
@@ -638,8 +638,16 @@
    @ApiOperation(value = "重置操作密码", tags = {"管理后台-系统用户管理"})
    public AjaxResult resetOperationPwd(@RequestBody Map<String, Object> params) {
        Long userId = parseLong(params.get("userId"));
        String operationPassword = parseString(params.get("operationPassword"));
        String code = parseString(params.get("code"));
        if (userId == null) {
            return AjaxResult.error("用户不存在");
        }
        if (StringUtils.isEmpty(operationPassword)) {
            return AjaxResult.error("请输入新操作密码");
        }
        if (StringUtils.isEmpty(code)) {
            return AjaxResult.error("请输入验证码");
        }
        SysUser user = userService.selectUserById(userId);
@@ -649,9 +657,19 @@
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(userId);
        user.setOperationPassword(SecurityUtils.encryptPassword("123456"));
        String cacheCode = redisService.getCacheObject(RESET_PWD_CODE_KEY + userId);
        if (StringUtils.isEmpty(cacheCode)) {
            return AjaxResult.error("验证码已过期,请重新获取验证码");
        }
        if (!code.equals(cacheCode)) {
            return AjaxResult.error("验证码错误");
        }
        user.setOperationPassword(SecurityUtils.encryptPassword(operationPassword));
        user.setUpdateBy(SecurityUtils.getUsername());
        return toAjax(userService.updateUser(user));
        int result = userService.updateUser(user);
        redisService.deleteObject(RESET_PWD_CODE_KEY + userId);
        return toAjax(result);
    }
    private Long parseLong(Object value) {
        if (value == null || StringUtils.isEmpty(String.valueOf(value))) {
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java
@@ -4,6 +4,7 @@
import com.ruoyi.chargingPile.api.dto.GetPermissionConfigurationDTO;
import com.ruoyi.chargingPile.api.dto.PartnerListDTO;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.chargingPile.api.model.TPartnerSite;
import com.ruoyi.chargingPile.api.model.TSiteMenu;
import com.ruoyi.chargingPile.api.query.GetPartnerList;
@@ -202,6 +203,11 @@
        return AjaxResult.success(partnerAppUserService.getBoundUsers(partnerId));
    }
    @PostMapping("/appUser/relations/{partnerId}")
    public R<List<TPartnerAppUser>> getBoundAppUserRelations(@PathVariable Integer partnerId) {
        return R.ok(partnerAppUserService.getRelations(partnerId));
    }
    @RequiresPermissions(value = {"/partners/update"}, logical = Logical.OR)
    @PostMapping("/appUser/bind")
    @ApiOperation(value = "保存合作商绑定的小程序用户", tags = {"管理后台-合作商管理"})
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java
@@ -15,6 +15,8 @@
    List<PartnerAppUserVO> getBoundUsers(Integer partnerId);
    List<TPartnerAppUser> getRelations(Integer partnerId);
    void saveBindings(PartnerAppUserBindDTO bindDTO);
    Partner getPartnerByAppUserId(Long appUserId);
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java
@@ -96,6 +96,14 @@
    }
    @Override
    public List<TPartnerAppUser> getRelations(Integer partnerId) {
        assertPartnerExists(partnerId);
        return list(new LambdaQueryWrapper<TPartnerAppUser>()
                .eq(TPartnerAppUser::getPartnerId, partnerId)
                .orderByAsc(TPartnerAppUser::getCreateTime));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveBindings(PartnerAppUserBindDTO bindDTO) {
        Integer partnerId = bindDTO.getPartnerId();
@@ -115,19 +123,35 @@
            }
        }
        remove(new LambdaQueryWrapper<TPartnerAppUser>().eq(TPartnerAppUser::getPartnerId, partnerId));
        if (selectedIds.isEmpty()) {
            return;
        List<TPartnerAppUser> existingRelations = list(new LambdaQueryWrapper<TPartnerAppUser>()
                .eq(TPartnerAppUser::getPartnerId, partnerId));
        Set<Long> existingIds = existingRelations.stream()
                .map(TPartnerAppUser::getAppUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Set<Long> removedIds = existingIds.stream()
                .filter(appUserId -> !selectedIds.contains(appUserId))
                .collect(Collectors.toSet());
        if (!removedIds.isEmpty()) {
            remove(new LambdaQueryWrapper<TPartnerAppUser>()
                    .eq(TPartnerAppUser::getPartnerId, partnerId)
                    .in(TPartnerAppUser::getAppUserId, removedIds));
        }
        List<TPartnerAppUser> relations = new ArrayList<>(selectedIds.size());
        List<TPartnerAppUser> relations = new ArrayList<>();
        for (Long appUserId : selectedIds) {
            if (existingIds.contains(appUserId)) {
                continue;
            }
            TPartnerAppUser relation = new TPartnerAppUser();
            relation.setPartnerId(partnerId);
            relation.setAppUserId(appUserId);
            relations.add(relation);
        }
        saveBatch(relations);
        if (!relations.isEmpty()) {
            saveBatch(relations);
        }
    }
    @Override
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
@@ -20,10 +20,12 @@
import com.ruoyi.chargingPile.api.domain.SiteMenu;
import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
import com.ruoyi.chargingPile.api.feignClient.*;
import com.ruoyi.chargingPile.api.model.Partner;
import com.ruoyi.chargingPile.api.model.Site;
import com.ruoyi.chargingPile.api.model.TChargingGun;
import com.ruoyi.chargingPile.api.model.TChargingPile;
import com.ruoyi.chargingPile.api.model.TParkingRecord;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.chargingPile.api.vo.GetChargingGunByChargingPileIdsVo;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.dto.ChargingOrderGroup;
@@ -57,6 +59,7 @@
import com.ruoyi.order.export.PayOrderExport;
import com.ruoyi.order.export.EnterpriseChargingDetailExport;
import com.ruoyi.order.service.*;
import com.ruoyi.order.support.FranchiseeUserStatistics;
import com.ruoyi.order.util.PreviousSixMonths;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.order.vo.EndOfChargePageInfo;
@@ -230,20 +233,22 @@
    @PostMapping("/app/franchisee/user")
    public R<Map<String, Object>> franchiseeUser(@RequestBody(required = false) Map<String, Object> params) {
        List<TChargingOrder> orders = franchiseeOrders(params);
        Set<Long> users = orders.stream().map(TChargingOrder::getAppUserId).filter(Objects::nonNull).collect(Collectors.toSet());
        List<Integer> siteIds = franchiseeSiteIds(params);
        Partner partner = franchiseePartner();
        List<Integer> siteIds = partner == null ? Collections.emptyList() : franchiseeSiteIds(params, partner.getId());
        LocalDateTime[] range = franchiseeRange(params);
        Set<Long> newUsers = new HashSet<>();
        if (!siteIds.isEmpty() && range[0] != null && range[1] != null) {
            List<TChargingOrder> allOrders = franchiseeOrders(siteIds, null, range[1]);
            Map<Long, LocalDateTime> firstOrder = new HashMap<>();
            for (TChargingOrder order : allOrders) {
                if (order.getAppUserId() == null || order.getEndTime() == null) continue;
                firstOrder.merge(order.getAppUserId(), order.getEndTime(), (oldTime, newTime) -> oldTime.isBefore(newTime) ? oldTime : newTime);
        List<TChargingOrder> orders = franchiseeOrders(siteIds, range[0], range[1]).stream()
                .filter(order -> Objects.equals(order.getStatus(), 5))
                .collect(Collectors.toList());
        Set<Long> users = orders.stream().map(TChargingOrder::getAppUserId).filter(Objects::nonNull).collect(Collectors.toSet());
        List<TPartnerAppUser> relations = Collections.emptyList();
        if (partner != null && partner.getId() != null) {
            R<List<TPartnerAppUser>> relationResult = partnerClient.getBoundAppUserRelations(partner.getId());
            if (relationResult != null && relationResult.getData() != null) {
                relations = relationResult.getData();
            }
            firstOrder.forEach((userId, firstTime) -> { if (!firstTime.isBefore(range[0])) newUsers.add(userId); });
        }
        FranchiseeUserStatistics.Result userStatistics = FranchiseeUserStatistics.calculate(
                orders, relations, range[0], range[1]);
        List<TAppUser> appUsers = Collections.emptyList();
        if (!users.isEmpty()) {
@@ -262,9 +267,8 @@
        Map<String, Object> evaluation = franchiseeEvaluation(siteIds);
        Map<String, Object> result = new LinkedHashMap<>();
        Map<String, Object> summary = new LinkedHashMap<>(); summary.put("chargeUserCount", users.size()); summary.put("newUserCount", newUsers.size());
        result.put("summary", summary);
        result.put("userStats", franchiseeUserTrend(orders, newUsers, range));
        result.put("summary", userStatistics.getSummary());
        result.put("userStats", userStatistics.getTrend());
        result.put("userTags", franchiseeUserTags(orders));
        Map<Integer, String> vipNames = new HashMap<>();
        R<List<TVip>> vipResult = vipClient.getAllVip();
@@ -319,40 +323,27 @@
    }
    private List<Integer> franchiseeSiteIds(Map<String, Object> params) {
        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) return Collections.emptyList();
        Long appUserId = tokenService.getLoginUserApplet().getUserId();
        R<com.ruoyi.chargingPile.api.model.Partner> partnerResult = partnerClient.getPartnerByAppUserId(appUserId);
        com.ruoyi.chargingPile.api.model.Partner partner = partnerResult == null ? null : partnerResult.getData();
        Partner partner = franchiseePartner();
        if (partner == null || partner.getId() == null) return Collections.emptyList();
        return franchiseeSiteIds(params, partner.getId());
    }
    private Partner franchiseePartner() {
        if (tokenService.getLoginUserApplet() == null || tokenService.getLoginUserApplet().getUserId() == null) return null;
        Long appUserId = tokenService.getLoginUserApplet().getUserId();
        R<Partner> partnerResult = partnerClient.getPartnerByAppUserId(appUserId);
        return partnerResult == null ? null : partnerResult.getData();
    }
    private List<Integer> franchiseeSiteIds(Map<String, Object> params, Integer partnerId) {
        R<List<Site>> siteResult = siteClient.getSiteAll();
        List<Site> sites = siteResult == null ? null : siteResult.getData();
        List<Integer> siteIds = sites == null ? new ArrayList<>() : sites.stream().filter(s -> Objects.equals(s.getPartnerId(), partner.getId())).map(Site::getId).collect(Collectors.toList());
        List<Integer> siteIds = sites == null ? new ArrayList<>() : sites.stream().filter(s -> Objects.equals(s.getPartnerId(), partnerId)).map(Site::getId).collect(Collectors.toList());
        Object requestedSite = params == null ? null : params.get("siteId");
        if (requestedSite != null && !String.valueOf(requestedSite).isEmpty()) {
            Integer siteId = Integer.valueOf(String.valueOf(requestedSite)); siteIds = siteIds.contains(siteId) ? Collections.singletonList(siteId) : Collections.singletonList(-1);
        }
        return siteIds;
    }
    private Map<String, Object> franchiseeUserTrend(List<TChargingOrder> orders, Set<Long> newUsers, LocalDateTime[] range) {
        LocalDate start = range[0] == null ? LocalDate.now() : range[0].toLocalDate();
        LocalDate end = range[1] == null ? start.plusDays(1) : range[1].toLocalDate();
        List<String> categories = new ArrayList<>();
        List<BigDecimal> chargeUsers = new ArrayList<>(), newUserCounts = new ArrayList<>();
        for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1)) {
            LocalDateTime dayStart = day.atStartOfDay(), dayEnd = day.plusDays(1).atStartOfDay();
            Set<Long> dayUsers = orders.stream().filter(order -> order.getAppUserId() != null && order.getEndTime() != null
                            && !order.getEndTime().isBefore(dayStart) && order.getEndTime().isBefore(dayEnd))
                    .map(TChargingOrder::getAppUserId).collect(Collectors.toSet());
            long newCount = dayUsers.stream().filter(newUsers::contains).count();
            categories.add(day.toString().substring(5));
            chargeUsers.add(BigDecimal.valueOf(dayUsers.size()));
            newUserCounts.add(BigDecimal.valueOf(newCount));
        }
        Map<String, Object> chart = new LinkedHashMap<>(); chart.put("categories", categories);
        Map<String, Object> chargeSeries = new LinkedHashMap<>(); chargeSeries.put("name", "充电用户"); chargeSeries.put("data", chargeUsers);
        Map<String, Object> newSeries = new LinkedHashMap<>(); newSeries.put("name", "新增用户"); newSeries.put("data", newUserCounts);
        chart.put("series", Arrays.asList(chargeSeries, newSeries)); return chart;
    }
    private List<Map<String, Object>> franchiseeUserTags(List<TChargingOrder> orders) {
@@ -892,6 +883,7 @@
                payOrderInfoDto.setThirdPayAmount(byId.getThirdPayAmount());
                payOrderInfoDto.setBalanceRefundAmount(byId.getBalanceRefundAmount());
                payOrderInfoDto.setThirdRefundAmount(byId.getThirdRefundAmount());
                payOrderInfoDto.setRefundableAmount(chargingOrderService.getManualRefundableAmount(byId.getId()));
                return R.ok(payOrderInfoDto);
            case 2:
                TShoppingOrder byId1 = shoppingOrderService.getById(orderId);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java
@@ -37,6 +37,8 @@
    private BigDecimal balanceRefundAmount;
    @ApiModelProperty("第三方退款金额")
    private BigDecimal thirdRefundAmount;
    @ApiModelProperty("剩余可人工退款金额(不扣减充电完成自动退款)")
    private BigDecimal refundableAmount;
}
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
@@ -215,6 +215,8 @@
    R specialRefund(PayOrderRefundDto payOrderQueryDto);
    BigDecimal getManualRefundableAmount(Long chargingOrderId);
    ChargingOrderListInfoVO chargingInfo(String uid);
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -4885,7 +4885,42 @@
        dto.setThirdPayAmount(order.getThirdPayAmount());
        dto.setBalanceRefundAmount(order.getBalanceRefundAmount());
        dto.setThirdRefundAmount(order.getThirdRefundAmount());
        dto.setRefundableAmount(getManualRefundableAmount(order));
        return R.ok(dto);
    }
    @Override
    public BigDecimal getManualRefundableAmount(Long chargingOrderId) {
        if (chargingOrderId == null) {
            return BigDecimal.ZERO;
        }
        return getManualRefundableAmount(this.getById(chargingOrderId));
    }
    private BigDecimal getManualRefundableAmount(TChargingOrder order) {
        if (order == null || order.getId() == null) {
            return BigDecimal.ZERO;
        }
        List<TChargingOrderRefund> successfulRefunds = chargingOrderRefundService.list(
                new LambdaQueryWrapper<TChargingOrderRefund>()
                        .eq(TChargingOrderRefund::getChargingOrderId, order.getId())
                        .eq(TChargingOrderRefund::getRefundStatus, 2));
        return calculateManualRefundableAmount(order.getPaymentAmount(), successfulRefunds);
    }
    static BigDecimal calculateManualRefundableAmount(BigDecimal paymentAmount, List<TChargingOrderRefund> successfulRefunds) {
        BigDecimal successfulManualRefundAmount = Optional.ofNullable(successfulRefunds)
                .orElse(Collections.emptyList())
                .stream()
                .filter(refund -> "后台退款".equals(refund.getRefundTitle())
                        || SPECIAL_REFUND_TITLE.equals(refund.getRefundTitle()))
                .map(TChargingOrderRefund::getRefundAmount)
                .map(amount -> amount == null ? BigDecimal.ZERO : amount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return (paymentAmount == null ? BigDecimal.ZERO : paymentAmount)
                .subtract(successfulManualRefundAmount)
                .max(BigDecimal.ZERO)
                .setScale(2, RoundingMode.HALF_UP);
    }
    @Override
@@ -4913,7 +4948,7 @@
            }
            mergeSuccessfulRefundAmounts(order);
            BigDecimal refundAmount = money(payOrderQueryDto.getRefundAmount());
            BigDecimal refundableAmount = money(order.getPaymentAmount()).subtract(money(order.getRefundAmount())).setScale(2, RoundingMode.HALF_UP);
            BigDecimal refundableAmount = getManualRefundableAmount(order);
            if (refundAmount.compareTo(refundableAmount) > 0) {
                return R.fail("退款金额不能超过剩余可退金额");
            }
@@ -5192,8 +5227,7 @@
                return R.fail("充电订单不存在");
            }
            BigDecimal manualRefundAmount = money(payOrderQueryDto.getRefundAmount());
            BigDecimal remainingRefundAmount = money(tChargingOrder.getPaymentAmount())
                    .subtract(money(tChargingOrder.getRefundAmount())).max(BigDecimal.ZERO);
            BigDecimal remainingRefundAmount = getManualRefundableAmount(tChargingOrder);
            if (manualRefundAmount.compareTo(remainingRefundAmount) > 0) {
                return R.fail("退款金额不能超过剩余可退金额" + remainingRefundAmount.toPlainString() + "元");
            }
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java
New file
@@ -0,0 +1,111 @@
package com.ruoyi.order.support;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.order.api.model.TChargingOrder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/** 加盟商看板用户统计的纯计算逻辑。 */
public final class FranchiseeUserStatistics {
    private FranchiseeUserStatistics() {
    }
    public static Result calculate(List<TChargingOrder> orders,
                                   List<TPartnerAppUser> relations,
                                   LocalDateTime start,
                                   LocalDateTime end) {
        List<TChargingOrder> safeOrders = orders == null ? Collections.emptyList() : orders;
        List<TPartnerAppUser> safeRelations = relations == null ? Collections.emptyList() : relations;
        Map<LocalDate, Set<Long>> chargingUsersByDay = new HashMap<>();
        Set<Long> chargingUsers = new HashSet<>();
        for (TChargingOrder order : safeOrders) {
            if (order == null || order.getAppUserId() == null || !inRange(order.getEndTime(), start, end)) {
                continue;
            }
            chargingUsers.add(order.getAppUserId());
            chargingUsersByDay.computeIfAbsent(order.getEndTime().toLocalDate(), key -> new HashSet<>())
                    .add(order.getAppUserId());
        }
        Map<Long, LocalDateTime> firstBindingByUser = new HashMap<>();
        for (TPartnerAppUser relation : safeRelations) {
            if (relation == null || relation.getAppUserId() == null || relation.getCreateTime() == null) {
                continue;
            }
            firstBindingByUser.merge(relation.getAppUserId(), relation.getCreateTime(),
                    (left, right) -> left.isBefore(right) ? left : right);
        }
        Map<LocalDate, Set<Long>> newUsersByDay = new HashMap<>();
        Set<Long> newUsers = new HashSet<>();
        firstBindingByUser.forEach((appUserId, createTime) -> {
            if (inRange(createTime, start, end)) {
                newUsers.add(appUserId);
                newUsersByDay.computeIfAbsent(createTime.toLocalDate(), key -> new HashSet<>()).add(appUserId);
            }
        });
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("chargeUserCount", (long) chargingUsers.size());
        summary.put("newUserCount", (long) newUsers.size());
        LocalDate firstDay = start == null ? LocalDate.now() : start.toLocalDate();
        LocalDate exclusiveEndDay = end == null ? firstDay.plusDays(1) : end.toLocalDate();
        List<String> categories = new ArrayList<>();
        List<Long> chargingSeries = new ArrayList<>();
        List<Long> newSeries = new ArrayList<>();
        for (LocalDate day = firstDay; day.isBefore(exclusiveEndDay); day = day.plusDays(1)) {
            categories.add(String.format("%02d-%02d", day.getMonthValue(), day.getDayOfMonth()));
            chargingSeries.add((long) chargingUsersByDay.getOrDefault(day, Collections.emptySet()).size());
            newSeries.add((long) newUsersByDay.getOrDefault(day, Collections.emptySet()).size());
        }
        Map<String, Object> trend = new LinkedHashMap<>();
        trend.put("categories", categories);
        trend.put("series", Arrays.asList(series("充电用户", chargingSeries), series("新增用户", newSeries)));
        return new Result(summary, trend);
    }
    private static boolean inRange(LocalDateTime time, LocalDateTime start, LocalDateTime end) {
        return time != null
                && (start == null || !time.isBefore(start))
                && (end == null || time.isBefore(end));
    }
    private static Map<String, Object> series(String name, List<Long> data) {
        Map<String, Object> series = new LinkedHashMap<>();
        series.put("name", name);
        series.put("data", data);
        return series;
    }
    public static final class Result {
        private final Map<String, Object> summary;
        private final Map<String, Object> trend;
        private Result(Map<String, Object> summary, Map<String, Object> trend) {
            this.summary = Objects.requireNonNull(summary);
            this.trend = Objects.requireNonNull(trend);
        }
        public Map<String, Object> getSummary() {
            return summary;
        }
        public Map<String, Object> getTrend() {
            return trend;
        }
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java
New file
@@ -0,0 +1,41 @@
package com.ruoyi.order.service.impl;
import com.ruoyi.order.api.model.TChargingOrderRefund;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TChargingOrderServiceImplRefundableAmountTest {
    @Test
    void completionRefundDoesNotReduceManualRefundableAmount() {
        TChargingOrderRefund completionRefund = refund("充电完成退款", "2.25");
        BigDecimal refundableAmount = TChargingOrderServiceImpl.calculateManualRefundableAmount(
                new BigDecimal("26.75"), Collections.singletonList(completionRefund));
        assertEquals(new BigDecimal("26.75"), refundableAmount);
    }
    @Test
    void onlySuccessfulManualRefundReducesManualRefundableAmount() {
        TChargingOrderRefund completionRefund = refund("充电完成退款", "2.25");
        TChargingOrderRefund manualRefund = refund("后台退款", "5.00");
        BigDecimal refundableAmount = TChargingOrderServiceImpl.calculateManualRefundableAmount(
                new BigDecimal("26.75"), Arrays.asList(completionRefund, manualRefund));
        assertEquals(new BigDecimal("21.75"), refundableAmount);
    }
    private TChargingOrderRefund refund(String title, String amount) {
        TChargingOrderRefund refund = new TChargingOrderRefund();
        refund.setRefundTitle(title);
        refund.setRefundAmount(new BigDecimal(amount));
        return refund;
    }
}
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java
New file
@@ -0,0 +1,74 @@
package com.ruoyi.order.support;
import com.ruoyi.chargingPile.api.model.TPartnerAppUser;
import com.ruoyi.order.api.model.TChargingOrder;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
class FranchiseeUserStatisticsTest {
    private static final LocalDateTime START = LocalDateTime.of(2026, 7, 1, 0, 0);
    private static final LocalDateTime END = LocalDateTime.of(2026, 7, 4, 0, 0);
    @Test
    void countsDistinctChargingUsersAndBucketsBindingsOnTheirExactDate() {
        List<TChargingOrder> orders = Arrays.asList(
                order(1L, LocalDateTime.of(2026, 7, 1, 8, 0)),
                order(1L, LocalDateTime.of(2026, 7, 1, 18, 0)),
                order(1L, LocalDateTime.of(2026, 7, 2, 8, 0)),
                order(2L, LocalDateTime.of(2026, 7, 2, 9, 0))
        );
        List<TPartnerAppUser> relations = Arrays.asList(
                relation(1L, LocalDateTime.of(2026, 6, 30, 23, 59)),
                relation(2L, LocalDateTime.of(2026, 7, 2, 10, 0)),
                relation(3L, LocalDateTime.of(2026, 7, 2, 11, 0)),
                relation(3L, LocalDateTime.of(2026, 7, 2, 12, 0))
        );
        FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculate(orders, relations, START, END);
        assertEquals(2L, result.getSummary().get("chargeUserCount"));
        assertEquals(2L, result.getSummary().get("newUserCount"));
        assertEquals(Arrays.asList("07-01", "07-02", "07-03"), result.getTrend().get("categories"));
        assertEquals(Arrays.asList(1L, 2L, 0L), seriesData(result.getTrend(), 0));
        assertEquals(Arrays.asList(0L, 2L, 0L), seriesData(result.getTrend(), 1));
    }
    @Test
    void returnsZeroFilledSeriesWhenThereIsNoActivity() {
        FranchiseeUserStatistics.Result result = FranchiseeUserStatistics.calculate(
                Collections.emptyList(), Collections.emptyList(), START, END);
        assertEquals(0L, result.getSummary().get("chargeUserCount"));
        assertEquals(0L, result.getSummary().get("newUserCount"));
        assertEquals(Arrays.asList(0L, 0L, 0L), seriesData(result.getTrend(), 0));
        assertEquals(Arrays.asList(0L, 0L, 0L), seriesData(result.getTrend(), 1));
    }
    @SuppressWarnings("unchecked")
    private List<Long> seriesData(Map<String, Object> trend, int index) {
        List<Map<String, Object>> series = (List<Map<String, Object>>) trend.get("series");
        return (List<Long>) series.get(index).get("data");
    }
    private TChargingOrder order(Long appUserId, LocalDateTime endTime) {
        TChargingOrder order = new TChargingOrder();
        order.setAppUserId(appUserId);
        order.setEndTime(endTime);
        return order;
    }
    private TPartnerAppUser relation(Long appUserId, LocalDateTime createTime) {
        TPartnerAppUser relation = new TPartnerAppUser();
        relation.setAppUserId(appUserId);
        relation.setCreateTime(createTime);
        return relation;
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
@@ -28,7 +28,7 @@
    @Resource
    private TDeviceService deviceService;
    @RequiresPermissions(value = {"/deviceManagement/list"}, logical = Logical.OR)
    @RequiresPermissions(value = {"/deviceManagement/list", "/faultInformationList"}, logical = Logical.OR)
    @ResponseBody
    @GetMapping("/pageList")
    @ApiOperation(value = "获取设备列表", tags = {"管理后台-设备管理"})
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java
@@ -22,7 +22,7 @@
public class TStationCollectController {
    @Resource private TStationCollectService stationCollectService;
    @RequiresPermissions(value = {"/stationCollect/list"}, logical = Logical.OR)
    @RequiresPermissions(value = {"/stationCollect/list", "/faultInformationList"}, logical = Logical.OR)
    @GetMapping("/pageList") @ApiOperation("分页查询逆变器")
    public AjaxResult<PageInfo<TStationCollect>> pageList(StationCollectQuery query) {
        return AjaxResult.success(stationCollectService.pageList(query));
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java
@@ -14,4 +14,5 @@
    @ApiModelProperty("机型型号") private String deviceModel;
    @ApiModelProperty("逆变器SN") private String inverterId;
    @ApiModelProperty("状态") private String status;
    @ApiModelProperty("是否只查询异常状态(离线、故障)") private Boolean abnormalOnly;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
@@ -61,6 +61,8 @@
                .like(StringUtils.isNotBlank(query.getDeviceModel()), TStationCollect::getDeviceModel, query.getDeviceModel())
                .like(StringUtils.isNotBlank(query.getInverterId()), TStationCollect::getInverterId, query.getInverterId())
                .eq(StringUtils.isNotBlank(query.getStatus()), TStationCollect::getStatus, query.getStatus())
                .in(StringUtils.isBlank(query.getStatus()) && Boolean.TRUE.equals(query.getAbnormalOnly()),
                        TStationCollect::getStatus, "2", "3")
                .orderByAsc(TStationCollect::getSortOrder).orderByDesc(TStationCollect::getCreateTime).page(page);
        fillStationAddress(page.getRecords());
        return page;