From cd4e26608b01cbc376760d96e2eab5d0a796f0b0 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期二, 28 七月 2026 10:14:04 +0800
Subject: [PATCH] 合并监控

---
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java                     |   40 ++++
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java                           |  111 +++++++++++++
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java |   41 +++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java                               |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java                    |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java                       |    2 
 ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java                |    4 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java                 |    6 
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java      |   34 +++
 ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java                             |   22 ++
 ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java               |    2 
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java                       |   74 +++++++++
 ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java           |    6 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java                              |    2 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java                                        |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java                                  |    1 
 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java                        |   72 ++++-----
 17 files changed, 371 insertions(+), 52 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java
index a05fa3f..d2cbd3a 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/factory/PartnerFallbackFactory.java
+++ b/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) {
diff --git a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java b/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java
index d8612e9..58cc55b 100644
--- a/ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/feignClient/PartnerClient.java
+++ b/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);
 	
 	
 	/**
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
index 1aea58e..9788e50 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
+++ b/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))) {
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java
index f5e665e..3ee7e42 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/controller/PartnerController.java
+++ b/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 = {"管理后台-合作商管理"})
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java
index 302e067..8a0f467 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/TPartnerAppUserService.java
+++ b/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);
diff --git a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java b/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java
index 010aa09..54e84c0 100644
--- a/ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/TPartnerAppUserServiceImpl.java
+++ b/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
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
index 9314981..5ca26f5 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TChargingOrderController.java
+++ b/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);
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java
index 6f38492..a017a52 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/dto/PayOrderInfoDto.java
+++ b/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;
 
 
 }
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
index 0fa01ea..976550e 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/TChargingOrderService.java
+++ b/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);
 
 
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
index e728c7b..ae56473 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
+++ b/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() + "元");
 			}
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java
new file mode 100644
index 0000000..77d8bcb
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/support/FranchiseeUserStatistics.java
@@ -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;
+        }
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java
new file mode 100644
index 0000000..65a00ad
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TChargingOrderServiceImplRefundableAmountTest.java
@@ -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;
+    }
+}
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java
new file mode 100644
index 0000000..3f9568e
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/support/FranchiseeUserStatisticsTest.java
@@ -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;
+    }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
index aa310b0..a0c3e58 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
+++ b/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 = {"管理后台-设备管理"})
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java
index 8a6b71c..b40bcbd 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationCollectController.java
+++ b/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));
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java
index 133cedf..2210a5b 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/StationCollectQuery.java
+++ b/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;
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
index e83841a..78bb1d8 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
+++ b/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;

--
Gitblit v1.7.1