From 61ed739b4ee4267653568a2aa91004ac6c43d248 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期三, 29 七月 2026 23:59:32 +0800
Subject: [PATCH] 各类修复

---
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java           |   41 +++++++---
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java |   57 ++++++++++----
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java     |   34 +++++---
 docs/superpowers/plans/2026-07-29-partner-user-permission-scope.md                                       |   51 ++++++++++++
 4 files changed, 139 insertions(+), 44 deletions(-)

diff --git a/docs/superpowers/plans/2026-07-29-partner-user-permission-scope.md b/docs/superpowers/plans/2026-07-29-partner-user-permission-scope.md
new file mode 100644
index 0000000..3344a4b
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-29-partner-user-permission-scope.md
@@ -0,0 +1,51 @@
+# Partner User Permission Scope Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Keep partner user-list data limited to users who charged at the partner's sites while allowing all user-management actions strictly according to configured permission keys.
+
+**Architecture:** Retain `PartnerAppUserAccessService.getVisibleUserIdsForCurrentPartner()` for list filtering and `assertUserVisible()` for object-level authorization. Remove the blanket partner read-only rule, apply visibility checks to every mutation targeting existing users, and remove frontend `isPartner` overrides so `Access` is the sole button-control mechanism.
+
+**Tech Stack:** Java 8, Spring Boot, MyBatis-Plus, React, Umi Access, JUnit 5, Mockito
+
+## Global Constraints
+
+- Partner accounts only see users who charged at their authorized sites.
+- Platform accounts retain unrestricted user-list scope.
+- View and mutation buttons follow their existing permission keys.
+- Partner mutations may only target users inside the partner-visible scope.
+- Do not weaken user-detail visibility checks.
+- Do not modify unrelated workspace changes.
+
+---
+
+### Task 1: Object-level partner authorization
+
+**Files:**
+- Modify: `ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java`
+- Modify: `ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java`
+- Test: `ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java`
+
+**Interfaces:**
+- Consumes: `getVisibleUserIdsForCurrentPartner()`.
+- Produces: `assertUsersVisible(Collection<Long> userIds)` for safe bulk mutations.
+
+- [x] **Step 1: Replace the read-only test with tests that allow authorized targets and reject targets outside partner scope.**
+- [x] **Step 2: Run the focused test and verify it fails before implementation.**
+- [x] **Step 3: Implement bulk visibility validation and remove `assertWritable()`.**
+- [x] **Step 4: Apply visibility validation to edit, freeze, unit change, delete, and gift-VIP mutations.**
+- [x] **Step 5: Run focused tests and compile the account module.**
+
+### Task 2: Frontend permission-only controls
+
+**Files:**
+- Modify: `chargingPile_management/src/pages/person/list/index.jsx`
+
+**Interfaces:**
+- Consumes: existing Umi `access` keys such as `/appUser/select`, `/appUser/update`, `/appUser/freeze`, `/appUser/del`, `/appUser/giftVip`, and `/appUser/updateCompany`.
+- Produces: buttons and row selection controlled only by configured permissions.
+
+- [x] **Step 1: Remove `isPartner` overrides around row actions, row selection, and toolbar rendering.**
+- [x] **Step 2: Keep every action wrapped by its existing `Access` permission check.**
+- [x] **Step 3: Run the frontend production build.**
+- [x] **Step 4: Review diffs to confirm the partner data filter and backend delete protections remain intact.**
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
index 53b0243..45cb758 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -696,8 +696,9 @@
     @PostMapping(value = "/user/addOrUpdate")
     @Log(title = "【用户列表】添加编辑用户", businessType = BusinessType.INSERT,operatorType = OperatorType.MANAGE)
     public R addOrUpdate(@RequestBody TAppUser  tAppUser) {
-        partnerAppUserAccessService.assertWritable();
         if (tAppUser.getId() != null){
+            AuthUtil.checkPermi("/appUser/update");
+            partnerAppUserAccessService.assertUserVisible(tAppUser.getId());
             //为编辑操作,要进行vip的逻辑处理
             TAppUser nowUser = appUserService.getById(tAppUser.getId());
             //会员结束时间不为空
@@ -731,6 +732,7 @@
             }
 
         }else {
+           AuthUtil.checkPermi("/appUser/add");
            List<TAppUser> list = appUserService.lambdaQuery().eq(TAppUser::getPhone, tAppUser.getPhone()).list();
            if (!list.isEmpty()){
                return R.fail("该手机号已注册");
@@ -955,7 +957,7 @@
     @PostMapping(value = "/user/status/change")
     @Log(title = "【用户列表】冻结解冻用户", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE)
     public R pointsDetail(@RequestBody UserChangeDto userChangeDto) {
-        partnerAppUserAccessService.assertWritable();
+        partnerAppUserAccessService.assertUserVisible(userChangeDto.getUserId());
         TAppUser appUser = appUserService.getById(userChangeDto.getUserId());
         appUser.setStatus(userChangeDto.getStatus());
         appUser.setRemark(userChangeDto.getRemark());
@@ -969,9 +971,10 @@
     @PostMapping(value = "/user/unit/change")
     @Log(title = "【用户列表】修改用户单位", businessType = BusinessType.UPDATE,operatorType = OperatorType.MANAGE)
     public R unitChange(@RequestBody UnitChangeDto unitChangeDto) {
-        partnerAppUserAccessService.assertWritable();
-        for (String s : unitChangeDto.getIds().split(",")) {
-            TAppUser byId = appUserService.getById(s);
+        List<Long> userIds = parseUserIds(unitChangeDto.getIds());
+        partnerAppUserAccessService.assertUsersVisible(userIds);
+        for (Long userId : userIds) {
+            TAppUser byId = appUserService.getById(userId);
             byId.setCompanyId(unitChangeDto.getUnitId());
             appUserService.updateById(byId);
         }
@@ -984,10 +987,10 @@
     @DeleteMapping(value = "/user/delete")
     @Log(title = "【用户列表】删除用户", businessType = BusinessType.DELETE,operatorType = OperatorType.MANAGE)
     public R userDelete(String ids) {
-        partnerAppUserAccessService.assertWritable();
-        String[] split = ids.split(",");
-        for (String s : split) {
-            appUserService.removeById(s);
+        List<Long> userIds = parseUserIds(ids);
+        partnerAppUserAccessService.assertUsersVisible(userIds);
+        for (Long userId : userIds) {
+            appUserService.removeById(userId);
         }
         return R.ok();
 
@@ -1164,10 +1167,10 @@
     @PostMapping(value = "/user/give/vip")
     @Log(title = "【用户列表】赠送会员", businessType = BusinessType.OTHER,operatorType = OperatorType.MANAGE)
     public R giveVip(@RequestBody GiveVipDto  giveVipDto) {
-        partnerAppUserAccessService.assertWritable();
-        String[] split = giveVipDto.getUserIds().split(",");
-        for (String s : split) {
-            TAppUser nowUser = appUserService.getById(s);
+        List<Long> userIds = parseUserIds(giveVipDto.getUserIds());
+        partnerAppUserAccessService.assertUsersVisible(userIds);
+        for (Long userId : userIds) {
+            TAppUser nowUser = appUserService.getById(userId);
             int plusDay = 0;
             if (giveVipDto.getType() == 1) {
                 plusDay = 1;
@@ -1204,6 +1207,18 @@
         }
         return R.ok();
     }
+
+    private List<Long> parseUserIds(String ids) {
+        if (StringUtils.isBlank(ids)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(ids.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotBlank)
+                .map(Long::valueOf)
+                .distinct()
+                .collect(Collectors.toList());
+    }
     
     
     
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
index d665936..34d98e1 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
+++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/PartnerAppUserAccessService.java
@@ -1,7 +1,7 @@
 package com.ruoyi.account.service;
 
-import com.ruoyi.chargingPile.api.domain.SiteMenu;
-import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
+import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
+import com.ruoyi.chargingPile.api.feignClient.SiteClient;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.exception.ServiceException;
 import com.ruoyi.common.security.service.TokenService;
@@ -14,10 +14,10 @@
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -29,14 +29,13 @@
 
     private static final Integer PARTNER_ROLE_TYPE = 2;
     private static final Long IMPOSSIBLE_USER_ID = -1L;
-    private static final String PARTNER_READ_ONLY_MESSAGE = "合作商账号仅支持查看用户信息";
 
     @Resource
     private TokenService tokenService;
     @Resource
     private SysUserClient sysUserClient;
     @Resource
-    private PartnerClient partnerClient;
+    private SiteClient siteClient;
     @Resource
     private ChargingOrderClient chargingOrderClient;
 
@@ -48,14 +47,16 @@
         if (!isPartner(sysUser)) {
             return null;
         }
-        if (sysUser.getObjectId() == null) {
+        if (sysUser.getObjectId() == null || sysUser.getUserId() == null) {
             return impossibleUserIds();
         }
-        R<Set<Integer>> siteResult = partnerClient.authSite(sysUser.getObjectId(), SiteMenu.SITE_LIST.getValue());
+        R<List<GetSiteListDTO>> siteResult = siteClient.getSiteListByUserId(sysUser.getUserId());
         if (siteResult == null || !R.isSuccess(siteResult) || siteResult.getData() == null) {
             return impossibleUserIds();
         }
         List<Integer> siteIds = siteResult.getData().stream()
+                .filter(Objects::nonNull)
+                .map(GetSiteListDTO::getId)
                 .filter(Objects::nonNull)
                 .filter(siteId -> siteId > 0)
                 .distinct()
@@ -76,10 +77,6 @@
         return userIds.isEmpty() ? impossibleUserIds() : userIds;
     }
 
-    public boolean isCurrentPartner() {
-        return isPartner(getCurrentSysUser());
-    }
-
     public void assertUserVisible(Long userId) {
         List<Long> visibleUserIds = getVisibleUserIdsForCurrentPartner();
         if (visibleUserIds != null && (userId == null || !visibleUserIds.contains(userId))) {
@@ -87,9 +84,18 @@
         }
     }
 
-    public void assertWritable() {
-        if (isCurrentPartner()) {
-            throw new ServiceException(PARTNER_READ_ONLY_MESSAGE);
+    public void assertUsersVisible(Collection<Long> userIds) {
+        List<Long> visibleUserIds = getVisibleUserIdsForCurrentPartner();
+        if (visibleUserIds == null) {
+            return;
+        }
+        if (userIds == null || userIds.isEmpty()) {
+            throw new ServiceException("无权操作所选用户");
+        }
+        for (Long userId : userIds) {
+            if (userId == null || !visibleUserIds.contains(userId)) {
+                throw new ServiceException("无权操作所选用户");
+            }
         }
     }
 
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
index f2aa127..870b557 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/PartnerAppUserAccessServiceTest.java
@@ -1,7 +1,7 @@
 package com.ruoyi.account.service;
 
-import com.ruoyi.chargingPile.api.domain.SiteMenu;
-import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
+import com.ruoyi.chargingPile.api.dto.GetSiteListDTO;
+import com.ruoyi.chargingPile.api.feignClient.SiteClient;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.exception.ServiceException;
 import com.ruoyi.common.security.service.TokenService;
@@ -18,10 +18,10 @@
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.verify;
@@ -33,7 +33,7 @@
 
     @Mock private TokenService tokenService;
     @Mock private SysUserClient sysUserClient;
-    @Mock private PartnerClient partnerClient;
+    @Mock private SiteClient siteClient;
     @Mock private ChargingOrderClient chargingOrderClient;
 
     private PartnerAppUserAccessService service;
@@ -43,15 +43,15 @@
         service = new PartnerAppUserAccessService();
         ReflectionTestUtils.setField(service, "tokenService", tokenService);
         ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
-        ReflectionTestUtils.setField(service, "partnerClient", partnerClient);
+        ReflectionTestUtils.setField(service, "siteClient", siteClient);
         ReflectionTestUtils.setField(service, "chargingOrderClient", chargingOrderClient);
     }
 
     @Test
     void partnerCanSeeOnlyUsersWhoChargedAtAuthorizedSites() {
         mockCurrentUser(2, 9);
-        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
-                .thenReturn(R.ok(new HashSet<>(Arrays.asList(11, 12))));
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Arrays.asList(site(11), site(12))));
         when(chargingOrderClient.getAppUserIdsBySiteIds(Arrays.asList(11, 12)))
                 .thenReturn(R.ok(Arrays.asList(101L, 102L, 101L)));
 
@@ -64,8 +64,8 @@
     @Test
     void partnerWithoutAuthorizedSitesGetsImpossibleUserId() {
         mockCurrentUser(2, 9);
-        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
-                .thenReturn(R.ok(Collections.singleton(-1)));
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Collections.singletonList(site(-1))));
 
         assertEquals(Collections.singletonList(-1L), service.getVisibleUserIdsForCurrentPartner());
         verifyNoInteractions(chargingOrderClient);
@@ -74,8 +74,8 @@
     @Test
     void orderServiceFailureDoesNotExposeAllUsers() {
         mockCurrentUser(2, 9);
-        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
-                .thenReturn(R.ok(Collections.singleton(11)));
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Collections.singletonList(site(11))));
         when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
                 .thenReturn(R.fail("order unavailable"));
 
@@ -87,14 +87,14 @@
         mockCurrentUser(1, null);
 
         assertNull(service.getVisibleUserIdsForCurrentPartner());
-        verifyNoInteractions(partnerClient, chargingOrderClient);
+        verifyNoInteractions(siteClient, chargingOrderClient);
     }
 
     @Test
     void partnerCannotReadUserOutsideVisibleScope() {
         mockCurrentUser(2, 9);
-        when(partnerClient.authSite(9, SiteMenu.SITE_LIST.getValue()))
-                .thenReturn(R.ok(Collections.singleton(11)));
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Collections.singletonList(site(11))));
         when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
                 .thenReturn(R.ok(Collections.singletonList(101L)));
 
@@ -102,16 +102,33 @@
     }
 
     @Test
-    void partnerUserManagementIsReadOnly() {
+    void partnerCanOperateUsersInsideVisibleScope() {
         mockCurrentUser(2, 9);
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Collections.singletonList(site(11))));
+        when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
+                .thenReturn(R.ok(Arrays.asList(101L, 102L)));
 
-        ServiceException exception = assertThrows(ServiceException.class, service::assertWritable);
+        assertDoesNotThrow(() -> service.assertUsersVisible(Arrays.asList(101L, 102L)));
+    }
 
-        assertEquals("合作商账号仅支持查看用户信息", exception.getMessage());
+    @Test
+    void partnerCannotOperateBulkUsersWhenOneIsOutsideVisibleScope() {
+        mockCurrentUser(2, 9);
+        when(siteClient.getSiteListByUserId(1L))
+                .thenReturn(R.ok(Collections.singletonList(site(11))));
+        when(chargingOrderClient.getAppUserIdsBySiteIds(Collections.singletonList(11)))
+                .thenReturn(R.ok(Collections.singletonList(101L)));
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> service.assertUsersVisible(Arrays.asList(101L, 999L)));
+
+        assertEquals("无权操作所选用户", exception.getMessage());
     }
 
     private void mockCurrentUser(Integer roleType, Integer objectId) {
         SysUser sysUser = new SysUser();
+        sysUser.setUserId(1L);
         sysUser.setRoleType(roleType);
         sysUser.setObjectId(objectId);
         LoginUser loginUser = new LoginUser();
@@ -119,4 +136,10 @@
         loginUser.setSysUser(sysUser);
         when(tokenService.getLoginUser()).thenReturn(loginUser);
     }
+
+    private GetSiteListDTO site(Integer id) {
+        GetSiteListDTO site = new GetSiteListDTO();
+        site.setId(id);
+        return site;
+    }
 }

--
Gitblit v1.7.1