puzhibing
2026-09-30 f52a9a4ef0ef574aff88acf28b49d58f5c733cfb
修改权限控制
2个文件已添加
4个文件已修改
361 ■■■■■ 已修改文件
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/controller/TGroupCompanyControllerTest.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImplTest.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260930_company_admin_menu_path_fix.sql 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260930_company_admin_role_menu.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java
@@ -8,6 +8,7 @@
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.service.CompanyAccountAccessService;
import com.ruoyi.other.service.TGroupCompanyService;
import io.swagger.annotations.ApiOperation;
import org.springframework.util.StringUtils;
@@ -41,9 +42,12 @@
    @Resource
    private TGroupCompanyService groupCompanyService;
    @Resource
    private CompanyAccountAccessService companyAccountAccessService;
    /**
     * 分页查询公司
     * <p>公司账号(roleType=5)只返回本公司数据,非公司账号保持原有查询范围</p>
     */
    @RequiresPermissions(value = {"/companyManagement/list", "/fleet/list", "/vinManagement/list", "/chargingCard/list"}, logical = Logical.OR)
    @GetMapping("/page")
@@ -56,7 +60,10 @@
                                                     @RequestParam(value = "contactPhone", required = false) String contactPhone,
                                                     @RequestParam(value = "status", required = false) Integer status) {
        PageInfo<TGroupCompany> page = new PageInfo<>(current, size);
        // 公司账号(roleType=5)只返回本公司数据;平台/合作商等账号保持原查询范围
        Integer currentCompanyId = companyAccountAccessService.getCurrentCompanyId();
        groupCompanyService.lambdaQuery()
                .eq(currentCompanyId != null, TGroupCompany::getId, currentCompanyId)
                .eq(groupId != null, TGroupCompany::getGroupId, groupId)
                .like(StringUtils.hasText(name), TGroupCompany::getName, name)
                .like(StringUtils.hasText(contactName), TGroupCompany::getContactName, contactName)
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
@@ -431,7 +431,7 @@
        if (old == null) {
            return AjaxResult.error("公司不存在");
        }
        // 编辑时账号只读(前端禁用),此处兼容外部调用方传入账号的场景:唯一性校验(排除自身)
        // 编辑支持修改登录账号:公司表唯一性校验(排除自身)
        if (StringUtils.hasText(company.getAccount())) {
            if (accountExists(company.getAccount(), company.getId())) {
                return AjaxResult.error("登录账号不能重复");
@@ -439,17 +439,36 @@
        }
        String account = StringUtils.hasText(company.getAccount()) ? company.getAccount() : old.getAccount();
        if (StringUtils.hasText(account)) {
            syncCompanyUserOnEdit(company, account);
            String error = syncCompanyUserOnEdit(company, old.getAccount(), account);
            if (error != null) {
                return AjaxResult.error(error);
            }
        }
        updateById(company);
        return AjaxResult.success();
    }
    /**
     * 编辑公司时同步登录账号:存在则仅更新账号资料(不修改密码),不存在则补建
     * 编辑公司时同步登录账号:按旧账号定位用户后更新为新账号(支持改账号);
     * 密码有值时重置(留空不修改);用户不存在则按新账号补建
     *
     * @return 冲突等业务错误信息,null 表示成功
     */
    private void syncCompanyUserOnEdit(TGroupCompany company, String account) {
        R<SysUser> userR = sysUserClient.queryUserByUserName(account);
    private String syncCompanyUserOnEdit(TGroupCompany company, String oldAccount, String newAccount) {
        // 账号变更时校验新账号未被其他用户占用(updateSysUser 无唯一性校验,需在此前置拦截)
        if (StringUtils.hasText(oldAccount) && !newAccount.equals(oldAccount)) {
            R<SysUser> occupantR = sysUserClient.queryUserByUserName(newAccount);
            SysUser occupant = occupantR == null ? null : occupantR.getData();
            if (occupant != null) {
                R<SysUser> ownerR = sysUserClient.queryUserByUserName(oldAccount);
                SysUser owner = ownerR == null ? null : ownerR.getData();
                if (owner == null || !occupant.getUserId().equals(owner.getUserId())) {
                    return "登录账号不能重复";
                }
            }
        }
        // 按旧账号定位既有用户(账号未变时与新账号一致),避免改名后误走补建导致双账号
        R<SysUser> userR = sysUserClient.queryUserByUserName(StringUtils.hasText(oldAccount) ? oldAccount : newAccount);
        SysUser user = userR == null ? null : userR.getData();
        if (user == null) {
            if (!StringUtils.hasText(company.getPassword())) {
@@ -457,7 +476,7 @@
            }
            Long roleId = resolveCompanyRoleId();
            SysUser newUser = new SysUser();
            newUser.setUserName(account);
            newUser.setUserName(newAccount);
            newUser.setNickName(company.getName());
            newUser.setPhonenumber(company.getContactPhone());
            newUser.setPassword(company.getPassword());
@@ -467,22 +486,33 @@
            newUser.setRoleType(COMPANY_ROLE_TYPE);
            newUser.setObjectId(company.getId());
            newUser.setPassWordUpdate(new Date());
            R<Long> r = sysUserClient.addSysUser(newUser);
            if (r == null || 200 != r.getCode()) {
                throw new RuntimeException(r == null ? "创建登录账号失败" : r.getMsg());
            R<Long> addR = sysUserClient.addSysUser(newUser);
            if (addR == null || 200 != addR.getCode()) {
                throw new RuntimeException(addR == null ? "创建登录账号失败" : addR.getMsg());
            }
            return;
            return null;
        }
        // 远程查询返回的密码字段可能是哈希或脱敏值,显式清空避免再次写回
        user.setPassword(null);
        user.setPassWordUpdate(null);
        user.setUserName(account);
        user.setUserName(newAccount);
        user.setNickName(company.getName());
        user.setPhonenumber(company.getContactPhone());
        R<Boolean> r = sysUserClient.updateSysUser(user);
        if (r == null || 200 != r.getCode()) {
            throw new RuntimeException(r == null ? "更新登录账号失败" : r.getMsg());
        R<Boolean> updateR = sysUserClient.updateSysUser(user);
        if (updateR == null || 200 != updateR.getCode()) {
            throw new RuntimeException(updateR == null ? "更新登录账号失败" : updateR.getMsg());
        }
        // 密码有值时重置(前端已 HMAC-MD5,由 system 侧 BCrypt 落库,与新增链路一致)
        if (StringUtils.hasText(company.getPassword())) {
            SysUser pwdUser = new SysUser();
            pwdUser.setUserId(user.getUserId());
            pwdUser.setPassword(company.getPassword());
            R pwdR = sysUserClient.resetPassword(pwdUser);
            if (pwdR == null || 200 != pwdR.getCode()) {
                throw new RuntimeException(pwdR == null ? "重置登录密码失败" : pwdR.getMsg());
            }
        }
        return null;
    }
    @Override
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/controller/TGroupCompanyControllerTest.java
New file
@@ -0,0 +1,71 @@
package com.ruoyi.other.controller;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.service.CompanyAccountAccessService;
import com.ruoyi.other.service.TGroupCompanyService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
 * 公司列表查询接口数据范围测试:公司账号(roleType=5)只查本公司数据。
 */
class TGroupCompanyControllerTest {
    private TGroupCompanyController controller;
    private CompanyAccountAccessService companyAccountAccessService;
    @SuppressWarnings("unchecked")
    private final LambdaQueryChainWrapper<TGroupCompany> chain =
            (LambdaQueryChainWrapper<TGroupCompany>) mock(LambdaQueryChainWrapper.class);
    @BeforeEach
    void setUp() {
        TGroupCompanyService groupCompanyService = mock(TGroupCompanyService.class);
        when(groupCompanyService.lambdaQuery()).thenReturn(chain);
        when(chain.eq(anyBoolean(), any(), any())).thenReturn(chain);
        when(chain.like(anyBoolean(), any(), any())).thenReturn(chain);
        when(chain.orderByDesc(any(SFunction.class))).thenReturn(chain);
        companyAccountAccessService = mock(CompanyAccountAccessService.class);
        controller = new TGroupCompanyController();
        ReflectionTestUtils.setField(controller, "groupCompanyService", groupCompanyService);
        ReflectionTestUtils.setField(controller, "companyAccountAccessService", companyAccountAccessService);
    }
    @Test
    @DisplayName("公司账号查询公司列表时强制追加本公司ID条件")
    void companyAccountQueriesOwnCompanyOnly() {
        when(companyAccountAccessService.getCurrentCompanyId()).thenReturn(9);
        AjaxResult<PageInfo<TGroupCompany>> result = controller.page(1, 10, null, null, null, null, null);
        assertNotNull(result);
        verify(chain).eq(eq(true), any(), eq(9));
    }
    @Test
    @DisplayName("非公司账号查询公司列表时不追加本公司ID条件")
    void platformAccountKeepsOriginalScope() {
        when(companyAccountAccessService.getCurrentCompanyId()).thenReturn(null);
        AjaxResult<PageInfo<TGroupCompany>> result = controller.page(1, 10, null, null, null, null, null);
        assertNotNull(result);
        verify(chain, never()).eq(eq(true), any(), any());
    }
}
ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImplTest.java
@@ -13,6 +13,7 @@
import com.ruoyi.other.api.model.TCompanyRechargeRecord;
import com.ruoyi.other.mapper.TCompanyRechargeRecordMapper;
import com.ruoyi.other.mapper.TFleetMapper;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysRoleClient;
import com.ruoyi.system.api.feignClient.SysUserClient;
import org.apache.ibatis.builder.MapperBuilderAssistant;
@@ -38,6 +39,9 @@
 * 公司详情(车队数量、本月充值金额)统计逻辑单测,不依赖 Spring 容器与数据库。
 */
class TGroupCompanyServiceImplTest {
    /** 编辑公司用例共享的远程账号客户端 mock,供 verify 使用 */
    private SysUserClient editUserClient;
    @BeforeAll
    static void initTableInfo() {
@@ -335,6 +339,134 @@
        return service;
    }
    /**
     * 编辑公司修改登录账号:按旧账号定位用户后把 userName 更新为新账号,
     * 不误走补建分支(避免双账号残留)
     */
    @Test
    void editCompanyUpdatesUserNameWhenAccountChanged() {
        TGroupCompany old = persistedCompany("company01");
        TGroupCompany edit = editCompanyBody();
        edit.setAccount("company02");
        TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), "company02", null);
        assertEquals(200, code(service.editCompany(edit)));
        ArgumentCaptor<SysUser> captor = ArgumentCaptor.forClass(SysUser.class);
        Mockito.verify(editUserClient).updateSysUser(captor.capture());
        assertEquals("company02", captor.getValue().getUserName());
        Mockito.verify(editUserClient, Mockito.never()).addSysUser(Mockito.any());
        assertEquals("company02", edit.getAccount());
    }
    /**
     * 编辑公司修改登录密码:调用 resetPassword 且仅携带 userId 与密码(前端已 HMAC-MD5)
     */
    @Test
    void editCompanyResetsPasswordWhenProvided() {
        TGroupCompany old = persistedCompany("company01");
        TGroupCompany edit = editCompanyBody();
        edit.setAccount("company01");
        edit.setPassword("hmac-value");
        TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), null, null);
        assertEquals(200, code(service.editCompany(edit)));
        ArgumentCaptor<SysUser> pwdCaptor = ArgumentCaptor.forClass(SysUser.class);
        Mockito.verify(editUserClient).resetPassword(pwdCaptor.capture());
        assertEquals(Long.valueOf(100L), pwdCaptor.getValue().getUserId());
        assertEquals("hmac-value", pwdCaptor.getValue().getPassword());
        // 资料更新调用不携带密码,避免哈希/脱敏值写回
        ArgumentCaptor<SysUser> userCaptor = ArgumentCaptor.forClass(SysUser.class);
        Mockito.verify(editUserClient).updateSysUser(userCaptor.capture());
        assertNull(userCaptor.getValue().getPassword());
    }
    /**
     * 新账号已被其他用户占用:返回错误且不更新任何数据
     */
    @Test
    void editCompanyRejectsAccountOccupiedByOtherUser() {
        TGroupCompany old = persistedCompany("company01");
        TGroupCompany edit = editCompanyBody();
        edit.setAccount("company02");
        TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L),
                "company02", owner(999L));
        assertEquals(500, code(service.editCompany(edit)));
        Mockito.verify(editUserClient, Mockito.never()).updateSysUser(Mockito.any());
        Mockito.verify(editUserClient, Mockito.never()).resetPassword(Mockito.any());
        Mockito.verify(service, Mockito.never()).updateById(Mockito.any());
    }
    /**
     * 编辑时密码留空:不调用 resetPassword,密码保持不变
     */
    @Test
    void editCompanyKeepsPasswordWhenAbsent() {
        TGroupCompany old = persistedCompany("company01");
        TGroupCompany edit = editCompanyBody();
        edit.setAccount("company01");
        TGroupCompanyServiceImpl service = buildEditCompanyService(old, "company01", owner(100L), null, null);
        assertEquals(200, code(service.editCompany(edit)));
        Mockito.verify(editUserClient, Mockito.never()).resetPassword(Mockito.any());
        Mockito.verify(editUserClient).updateSysUser(Mockito.any(SysUser.class));
    }
    private TGroupCompany persistedCompany(String account) {
        TGroupCompany company = new TGroupCompany();
        company.setId(1);
        company.setAccount(account);
        company.setName("旧公司名");
        return company;
    }
    private TGroupCompany editCompanyBody() {
        TGroupCompany company = new TGroupCompany();
        company.setId(1);
        company.setName("新公司名");
        company.setContactPhone("13800000000");
        return company;
    }
    private SysUser owner(Long userId) {
        SysUser user = new SysUser();
        user.setUserId(userId);
        user.setPassword("hashed");
        return user;
    }
    /**
     * 构建编辑公司场景的 spy:getById 返回库里旧记录,updateById 打桩;
     * queryUserByUserName 按账号返回不同用户(newAccountUser 非 null 时用于模拟新账号占用)
     */
    private TGroupCompanyServiceImpl buildEditCompanyService(TGroupCompany old, String oldAccount, SysUser oldAccountUser,
                                                             String newAccount, SysUser newAccountUser) {
        SysUserClient sysUserClient = Mockito.mock(SysUserClient.class);
        Mockito.when(sysUserClient.queryUserByUserName(oldAccount)).thenReturn(R.ok(oldAccountUser));
        if (newAccount != null) {
            Mockito.when(sysUserClient.queryUserByUserName(newAccount)).thenReturn(R.ok(newAccountUser));
        }
        Mockito.when(sysUserClient.updateSysUser(Mockito.any())).thenReturn(R.ok(true));
        Mockito.when(sysUserClient.resetPassword(Mockito.any())).thenReturn(R.ok());
        this.editUserClient = sysUserClient;
        TGroupCompanyServiceImpl service = Mockito.spy(new TGroupCompanyServiceImpl());
        Mockito.doReturn(old).when(service).getById(Mockito.any());
        Mockito.doReturn(true).when(service).updateById(Mockito.any());
        Mockito.doReturn(0L).when(service).count(Mockito.any());
        ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
        return service;
    }
    private int code(AjaxResult result) {
        return ((Number) result.get("code")).intValue();
    }
sql/20260930_company_admin_menu_path_fix.sql
New file
@@ -0,0 +1,84 @@
-- =============================================================================
-- 2026-09-30 公司管理员菜单 path 与权限键(perms)对齐修复
-- -----------------------------------------------------------------------------
-- 背景:后端登录下发的权限集合取自 sys_menu.path(SysMenuMapper.selectMenuPermsByUserId
--       查询的是 m.path),本项目约定「菜单 path = 前端 accessKey = 接口权限键」。
--       20260930_company_admin_role_menu.sql 早期版本把新建菜单的 path 写成
--       '#'(按钮)、'list'(列表页)、'/card'(卡片管理目录),与权限键不一致,导致:
--         1) 公司账号登录后 permissions 缺 /fleet/list、/vinManagement/list、
--            /chargingCard/list 等键,车队/VIN/卡片菜单整块不显示(仅白名单可见);
--         2) /other/group-company/page 等接口鉴权失败(提示“没有权限”)。
-- 处理:按 perms 定位(不依赖 menu_id,跨环境安全),把相关菜单 path 改写为 perms。
-- 说明:仅更新 sys_menu.path 一列,不改菜单绑定关系;语句幂等,可重复执行;
--       与 20260930_company_admin_role_menu.sql 配合执行,两者先后顺序均可。
-- =============================================================================
USE charging_pile_other;
SET NAMES utf8mb4;
-- 1. 车队管理(列表页 + 6 个按钮)
UPDATE sys_menu SET path = '/fleet/list'   WHERE perms = '/fleet/list'   AND (path IS NULL OR path <> '/fleet/list');
UPDATE sys_menu SET path = '/fleet/add'    WHERE perms = '/fleet/add'    AND (path IS NULL OR path <> '/fleet/add');
UPDATE sys_menu SET path = '/fleet/edit'   WHERE perms = '/fleet/edit'   AND (path IS NULL OR path <> '/fleet/edit');
UPDATE sys_menu SET path = '/fleet/delete' WHERE perms = '/fleet/delete' AND (path IS NULL OR path <> '/fleet/delete');
UPDATE sys_menu SET path = '/fleet/status' WHERE perms = '/fleet/status' AND (path IS NULL OR path <> '/fleet/status');
UPDATE sys_menu SET path = '/fleet/import' WHERE perms = '/fleet/import' AND (path IS NULL OR path <> '/fleet/import');
UPDATE sys_menu SET path = '/fleet/export' WHERE perms = '/fleet/export' AND (path IS NULL OR path <> '/fleet/export');
-- 2. VIN 码管理(列表页 + 8 个按钮)
UPDATE sys_menu SET path = '/vinManagement/list'     WHERE perms = '/vinManagement/list'     AND (path IS NULL OR path <> '/vinManagement/list');
UPDATE sys_menu SET path = '/vinManagement/add'      WHERE perms = '/vinManagement/add'      AND (path IS NULL OR path <> '/vinManagement/add');
UPDATE sys_menu SET path = '/vinManagement/edit'     WHERE perms = '/vinManagement/edit'     AND (path IS NULL OR path <> '/vinManagement/edit');
UPDATE sys_menu SET path = '/vinManagement/delete'   WHERE perms = '/vinManagement/delete'   AND (path IS NULL OR path <> '/vinManagement/delete');
UPDATE sys_menu SET path = '/vinManagement/status'   WHERE perms = '/vinManagement/status'   AND (path IS NULL OR path <> '/vinManagement/status');
UPDATE sys_menu SET path = '/vinManagement/import'   WHERE perms = '/vinManagement/import'   AND (path IS NULL OR path <> '/vinManagement/import');
UPDATE sys_menu SET path = '/vinManagement/export'   WHERE perms = '/vinManagement/export'   AND (path IS NULL OR path <> '/vinManagement/export');
UPDATE sys_menu SET path = '/vinManagement/allocate' WHERE perms = '/vinManagement/allocate' AND (path IS NULL OR path <> '/vinManagement/allocate');
UPDATE sys_menu SET path = '/vinManagement/unbind'   WHERE perms = '/vinManagement/unbind'   AND (path IS NULL OR path <> '/vinManagement/unbind');
-- 3. 卡片管理(目录与列表页 path 同为 /chargingCard/list;含 6+2+2+1 个按钮)
UPDATE sys_menu SET path = '/chargingCard/list'     WHERE perms = '/chargingCard/list'     AND (path IS NULL OR path <> '/chargingCard/list');
UPDATE sys_menu SET path = '/chargingCard/add'      WHERE perms = '/chargingCard/add'      AND (path IS NULL OR path <> '/chargingCard/add');
UPDATE sys_menu SET path = '/chargingCard/edit'     WHERE perms = '/chargingCard/edit'     AND (path IS NULL OR path <> '/chargingCard/edit');
UPDATE sys_menu SET path = '/chargingCard/delete'   WHERE perms = '/chargingCard/delete'   AND (path IS NULL OR path <> '/chargingCard/delete');
UPDATE sys_menu SET path = '/chargingCard/status'   WHERE perms = '/chargingCard/status'   AND (path IS NULL OR path <> '/chargingCard/status');
UPDATE sys_menu SET path = '/chargingCard/import'   WHERE perms = '/chargingCard/import'   AND (path IS NULL OR path <> '/chargingCard/import');
UPDATE sys_menu SET path = '/chargingCard/export'   WHERE perms = '/chargingCard/export'   AND (path IS NULL OR path <> '/chargingCard/export');
UPDATE sys_menu SET path = '/chargingCard/allocate' WHERE perms = '/chargingCard/allocate' AND (path IS NULL OR path <> '/chargingCard/allocate');
UPDATE sys_menu SET path = '/chargingCard/unbind'   WHERE perms = '/chargingCard/unbind'   AND (path IS NULL OR path <> '/chargingCard/unbind');
-- 4. 白名单用户(导出/启停/解绑/额度分配;其余按钮 path 与 perms 原已一致)
UPDATE sys_menu SET path = '/appUserWhitelist/export'   WHERE perms = '/appUserWhitelist/export'   AND (path IS NULL OR path <> '/appUserWhitelist/export');
UPDATE sys_menu SET path = '/appUserWhitelist/status'   WHERE perms = '/appUserWhitelist/status'   AND (path IS NULL OR path <> '/appUserWhitelist/status');
UPDATE sys_menu SET path = '/appUserWhitelist/unbind'   WHERE perms = '/appUserWhitelist/unbind'   AND (path IS NULL OR path <> '/appUserWhitelist/unbind');
UPDATE sys_menu SET path = '/appUserWhitelist/allocate' WHERE perms = '/appUserWhitelist/allocate' AND (path IS NULL OR path <> '/appUserWhitelist/allocate');
-- 5. 卡片管理其余子页(刷卡充电订单、卡片收支明细):同批菜单同类修复,path 与权限键对齐
--    (公司账号未绑定这两个子页,此处修复的是平台/其他角色绑定的历史菜单)
UPDATE sys_menu SET path = '/cardChargingOrder'                WHERE perms = '/cardChargingOrder'                AND (path IS NULL OR path <> '/cardChargingOrder');
UPDATE sys_menu SET path = '/cardChargingOrder/detail'         WHERE perms = '/cardChargingOrder/detail'         AND (path IS NULL OR path <> '/cardChargingOrder/detail');
UPDATE sys_menu SET path = '/chargingCardBalanceDetail/list'   WHERE perms = '/chargingCardBalanceDetail/list'   AND (path IS NULL OR path <> '/chargingCardBalanceDetail/list');
UPDATE sys_menu SET path = '/chargingCardBalanceDetail/export' WHERE perms = '/chargingCardBalanceDetail/export' AND (path IS NULL OR path <> '/chargingCardBalanceDetail/export');
-- =============================================================================
-- 回查语句(人工核对,不参与脚本执行)
-- =============================================================================
-- R1. 期望 0:以上权限键对应菜单仍有 path <> perms 的记录
--   SELECT COUNT(*) FROM sys_menu WHERE perms IS NOT NULL AND (path IS NULL OR path <> perms)
--     AND (perms LIKE '/fleet%' OR perms LIKE '/vinManagement%'
--          OR perms LIKE '/chargingCard%' OR perms LIKE '/appUserWhitelist%');
-- R2. 公司账号权限集合(等价 selectMenuPermsByUserId,<userId> 替换为公司账号 user_id)
--   SELECT DISTINCT m.path FROM sys_menu m
--     JOIN sys_role_menu rm ON rm.menu_id = m.menu_id
--     JOIN sys_user_role ur ON ur.role_id = rm.role_id
--     JOIN sys_role r ON r.role_id = ur.role_id
--     WHERE m.status = '0' AND r.status = '0' AND ur.user_id = <userId>
--     ORDER BY m.path;
-- R3. 关键键命中(期望 7 行):/appUser、/appUserWhitelist/list、/chargingCard/list、
--     /fleet、/fleet/list、/vinManagement、/vinManagement/list
--   SELECT m.path FROM sys_menu m
--     JOIN sys_role_menu rm ON rm.menu_id = m.menu_id
--     JOIN sys_user_role ur ON ur.role_id = rm.role_id
--     JOIN sys_role r ON r.role_id = ur.role_id
--     WHERE m.status = '0' AND r.status = '0' AND ur.user_id = <userId>
--       AND m.path IN ('/appUser','/appUserWhitelist/list','/chargingCard/list',
--                      '/fleet','/fleet/list','/vinManagement','/vinManagement/list');
sql/20260930_company_admin_role_menu.sql
@@ -8,6 +8,13 @@
--   4) company_admin 默认菜单 = 车队管理 + VIN码管理 + 卡片管理(不含刷卡充电订单、
--      卡片收支明细子页)+ 白名单用户(含全部操作按钮),白名单挂载在「用户管理」下
--   5) 旧 company 角色不物理删除,迁移绑定后停用(status=1)
--   6) 【重要】后端登录下发的权限集合取自 sys_menu.path(SysMenuMapper.selectMenuPermsByUserId
--      查询的是 m.path),本项目约定「菜单 path = 前端 accessKey = 接口权限键」:
--        M 目录 path = 前端目录 accessKey(如 /fleet);
--        C 列表页 path = 前端页面 accessKey(如 /fleet/list);
--        F 按钮 path = 该按钮权限键(如 /fleet/add;沿用若依默认的 '#' 会导致权限键丢失)。
--      本脚本执行后请继续执行 ChargingPile/sql/20260930_company_admin_menu_path_fix.sql,
--      修正历史菜单 path(列表页 'list'、按钮 '#'、卡片管理目录 '/card')。
-- =============================================================================
SET NAMES utf8mb4;
@@ -22,7 +29,7 @@
-- 1.2 车队列表(菜单 C)
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT '车队列表', m.menu_id, 1, 'list', 'fleet', NULL, 1, 0, 'C', '0', '0', '/fleet/list', 'list', 'admin', NOW(), '公司管理员默认菜单'
SELECT '车队列表', m.menu_id, 1, '/fleet/list', 'fleet', NULL, 1, 0, 'C', '0', '0', '/fleet/list', 'list', 'admin', NOW(), '公司管理员默认菜单'
FROM sys_menu m
WHERE m.menu_type = 'M' AND m.path = '/fleet'
  AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_type = 'C' AND perms = '/fleet/list');