21 小时以前 8438d50aef0bb78e5e6c41ca1c1bc09b4e61add1
feat(customer): 添加公海客户归属查看权限控制

- 在CustomerMapper中增加isAdmin参数用于权限判断
- 修改数据库查询逻辑实现权限控制的差异化显示
- 重构CustomerServiceImpl中的客户列表查询方法
- 新增私海客户新增时同步到公海的功能
- 更新AI销售助手工具类的客户查询权限验证
- 添加customer:viewAll权限标识及相关文档说明
- 增加数据库迁移脚本配置权限菜单项
已添加2个文件
已修改4个文件
174 ■■■■■ 文件已修改
docs/customer_view_all_permission.md 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/migration_customer_view_all_permission.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/customer_view_all_permission.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
# å…¬æµ·å®¢æˆ·å½’属查看权限
## æ¶‰åŠé¡µé¢
- å®¢æˆ·æ¡£æ¡ˆ - å…¬æµ·å®¢æˆ·åˆ—表 (`/customer/publicSea`)
- å®¢æˆ·æ¡£æ¡ˆ - ç§æµ·å®¢æˆ·åˆ—表 (`/customer/privateSea`)
- AI é”€å”®åŠ©æ‰‹ - å®¢æˆ·åˆ—表查询
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /basic/customer/list | æŸ¥è¯¢å®¢æˆ·åˆ—表(type=1 å…¬æµ·ï¼Œtype=0 ç§æµ·ï¼‰ |
**请求参数:** ä¸å˜ï¼Œå‚考现有接口文档。
**响应变更:** `usageUserName` å­—段的值受权限控制:
- **有 `customer:viewAll` æƒé™**:`usageUserName` è¿”回该客户对应的销售人名称(与之前行为一致)
- **无 `customer:viewAll` æƒé™**:仅当客户由当前用户领用(`usage_user`)或创建(`create_user`)时返回销售人名称,其他情况返回空字符串 `""`
响应结构不变,仍为:
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "id": 1,
        "customerName": "xxx公司",
        "usageUserName": "张三",
        "type": 1,
        "isAssigned": 1
      }
    ]
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å…¬æµ·å®¢æˆ·åˆ—表表格 â€” å¤„理空的 usageUserName
公海列表的"归属销售"列需处理 `usageUserName` ä¸ºç©ºçš„æƒ…况:
```html
<el-table-column prop="usageUserName" label="归属销售" width="120">
  <template slot-scope="scope">
    <span>{{ scope.row.usageUserName || '-' }}</span>
  </template>
</el-table-column>
```
### 2. æƒé™æŽ§åˆ¶ â€” é¡µé¢æŒ‰é’®/功能可见性
通过 `v-hasPermi` æŒ‡ä»¤æˆ– `@ss.hasPermi('customer:viewAll')` æŽ§åˆ¶ç®¡ç†ç±»åŠŸèƒ½çš„å¯è§æ€§ï¼ˆä¾‹å¦‚å¯¼å‡ºå«é”€å”®äººä¿¡æ¯çš„æŠ¥è¡¨ç­‰ï¼‰ï¼š
```html
<el-button v-hasPermi="['customer:viewAll']" @click="exportWithSalesperson">
  å¯¼å‡ºï¼ˆå«å½’属销售)
</el-button>
```
### 3. æƒé™æ ‡è¯†
在权限管理(角色管理 > èœå•权限)中新增按钮级权限:
| æƒé™æ ‡è¯† | æƒé™åç§° | è¯´æ˜Ž |
|----------|----------|------|
| `customer:viewAll` | å…¬æµ·å®¢æˆ·å½’属查看 | å…è®¸æŸ¥çœ‹å…¬æµ·ä¸­æ‰€æœ‰å®¢æˆ·çš„归属销售人信息 |
## æ³¨æ„äº‹é¡¹
- è¯¥æƒé™ä¸å½±å“å®¢æˆ·æ•°æ®çš„可见范围(公海客户仍对所有登录用户可见),只控制"归属销售人"信息的展示。
- ç§æµ·åˆ—表中,用户原本就只能看到自己的客户,`usageUserName` å§‹ç»ˆæ˜¯è‡ªå·±çš„名称,不受此权限影响。
- å‰ç«¯ä¸‹æ‹‰é€‰æ‹©å®¢æˆ·åˆ—表 (`/basic/customer/customerList`) ä¹Ÿå—此权限影响,无权限时 `usageUserName` å¯èƒ½ä¸ºç©ºã€‚
- AI é”€å”®åŠ©æ‰‹çš„å®¢æˆ·æŸ¥è¯¢åŒæ ·éµå¾ªæ­¤æƒé™è§„åˆ™ã€‚
sql/migration_customer_view_all_permission.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
-- ----------------------------
-- å®¢æˆ·æ¡£æ¡ˆ - å…¬æµ·å®¢æˆ·å½’属查看权限
-- è¯´æ˜Žï¼šåœ¨"客户档案"菜单下新增按钮权限,有该权限的用户可在公海查看所有客户的归属销售人信息
-- æ‰§è¡Œæ–¹å¼ï¼šç›´æŽ¥æ‰§è¡Œæœ¬ SQL æˆ–通过系统菜单管理界面手动添加
-- ----------------------------
-- æŸ¥æ‰¾"客户档案"菜单的 ID ä½œä¸ºçˆ¶èœå•
SET @parent_menu_name = '客户档案(公海)';
SELECT @parent_id := menu_id
FROM sys_menu
WHERE menu_name = @parent_menu_name
  AND menu_type IN ('C', 'M')
LIMIT 1;
-- ä»…当找到父菜单且该权限标识不存在时才插入
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark, app_component)
SELECT '公海客户归属查看',
       @parent_id,
       9,
       '',
       NULL,
       NULL,
       '',
       1,
       0,
       'F',
       '0',
       '0',
       'customer:viewAll',
       '#',
       'admin',
       NOW(),
       '',
       NULL,
       '控制公海客户列表中归属销售人名称的可见性',
       NULL
FROM DUAL
WHERE @parent_id IS NOT NULL
  AND NOT EXISTS (
    SELECT 1 FROM sys_menu WHERE perms = 'customer:viewAll'
  );
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
@@ -81,7 +81,8 @@
        customerDto.setType(normalizeSeaType(seaType));
        customerDto.setUsageStatus(1L);
        List<CustomerVo> rows = defaultList(customerMapper.list(customerDto, loginUser.getUserId()));
        boolean isAdmin = SecurityUtils.hasPermi("customer:viewAll");
        List<CustomerVo> rows = defaultList(customerMapper.list(customerDto, loginUser.getUserId(), isAdmin));
        List<CustomerVo> filtered = rows.stream()
                .filter(item -> matchCustomerKeyword(item, keyword))
                .sorted(Comparator.comparing(CustomerVo::getId, Comparator.nullsLast(Comparator.reverseOrder())))
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java
@@ -70,9 +70,9 @@
     */
    int deleteCustomerByIds(Long[] ids);
    IPage<CustomerVo> listPage(Page<CustomerDto> page, @Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
    IPage<CustomerVo> listPage(Page<CustomerDto> page, @Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId, @Param("isAdmin") Boolean isAdmin);
    List<CustomerVo> list(@Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
    List<CustomerVo> list(@Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId, @Param("isAdmin") Boolean isAdmin);
    IPage<CustomerTransactionsVo> customewTransactions(Page page, @Param("customerName") String customerName);
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -130,7 +130,8 @@
    public IPage<CustomerVo> selectCustomerList(Page<CustomerDto> page, CustomerDto customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long loginUserId = loginUser.getUserId();
        IPage<CustomerVo> customerPage = customerMapper.listPage(page, customer, loginUserId);
        boolean isAdmin = SecurityUtils.hasPermi("customer:viewAll");
        IPage<CustomerVo> customerPage = customerMapper.listPage(page, customer, loginUserId, isAdmin);
        List<CustomerVo> records = customerPage.getRecords();
        if (CollectionUtils.isEmpty(records)) {
@@ -199,7 +200,21 @@
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long tenantId = loginUser.getTenantId();
        customer.setTenantId(tenantId);
        return customerMapper.insert(customer);
        int result = customerMapper.insert(customer);
        // ç§æµ·æ–°å¢žæ—¶åŒæ­¥ä¸€æ¡åˆ°å…¬æµ·
        if (customer.getType() != null && customer.getType() == 0) {
            Customer publicSeaCustomer = new Customer();
            BeanUtils.copyProperties(customer, publicSeaCustomer);
            publicSeaCustomer.setId(null);
            publicSeaCustomer.setType(1);
            publicSeaCustomer.setIsAssigned(0);
            publicSeaCustomer.setUsageUser(loginUser.getUserId());
            publicSeaCustomer.setUsageStatus(1L);
            customerMapper.insert(publicSeaCustomer);
        }
        return result;
    }
    /**
@@ -281,7 +296,8 @@
    public List<CustomerVo> selectCustomerLists(CustomerDto customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long loginUserId = loginUser.getUserId();
        return customerMapper.list(customer, loginUserId);
        boolean isAdmin = SecurityUtils.hasPermi("customer:viewAll");
        return customerMapper.list(customer, loginUserId, isAdmin);
    }
    @Override
@@ -312,13 +328,14 @@
    public List<Map<String, Object>> customerList(Customer customer) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        Long loginUserId = loginUser.getUserId();
        boolean isAdmin = SecurityUtils.hasPermi("customer:viewAll");
        CustomerDto query = new CustomerDto();
        if (customer != null) {
            BeanUtils.copyProperties(customer, query);
        }
        // å¤ç”¨å®¢æˆ·åˆ—表的可见性规则,避免把别人的私海客户暴露给当前账号
        List<CustomerVo> visibleCustomers = customerMapper.list(query, loginUserId);
        List<CustomerVo> visibleCustomers = customerMapper.list(query, loginUserId, isAdmin);
        if (CollectionUtils.isEmpty(visibleCustomers)) {
            return Collections.emptyList();
        }
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -9,9 +9,16 @@
    <select id="listPage" resultType="com.ruoyi.basic.vo.CustomerVo">
        select
        c.*,
        u.user_name usage_user_name,
        <choose>
            <when test="isAdmin">
                u.nick_name usage_user_name,
            </when>
            <otherwise>
                CASE WHEN c.usage_user = #{loginUserId} OR c.create_user = #{loginUserId} THEN u.nick_name ELSE '' END as usage_user_name,
            </otherwise>
        </choose>
        (
        select group_concat(u2.user_name separator ', ')
        select group_concat(u2.nick_name separator ', ')
        from customer_user cu
        left join sys_user u2 on cu.user_id = u2.user_id
        where cu.customer_id = c.id
@@ -65,9 +72,16 @@
    <select id="list" resultType="com.ruoyi.basic.vo.CustomerVo">
        select
        c.*,
        u.user_name usage_user_name,
        <choose>
            <when test="isAdmin">
                u.nick_name usage_user_name,
            </when>
            <otherwise>
                CASE WHEN c.usage_user = #{loginUserId} OR c.create_user = #{loginUserId} THEN u.nick_name ELSE '' END as usage_user_name,
            </otherwise>
        </choose>
        (
        select group_concat(u2.user_name separator ', ')
        select group_concat(u2.nick_name separator ', ')
        from customer_user cu
        left join sys_user u2 on cu.user_id = u2.user_id
        where cu.customer_id = c.id