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