Files
cattleTransportation/权限管理和运送清单新增功能实施计划.md
2025-10-13 17:19:47 +08:00

58 KiB
Raw Blame History

权限管理和运送清单新增功能实施计划

项目名称: 牛只运输管理系统
文档版本: V1.0
创建日期: 2025-10-11
项目路径: c:\cattleTransport


一、项目概述

1.1 背景说明

本项目旨在完善牛只运输管理系统的权限管理机制,并新增运送清单创建功能。系统采用前后端分离架构:

  • 前端: Vue3 + TypeScript + Pinia + Element Plus
  • 后端: Spring Boot 2.6.13 + MyBatis-Plus + Sa-Token
  • 数据库: MySQL + Redis

1.2 核心目标

  1. 实现基于角色的菜单权限管理和动态路由
  2. 实现基于权限码的操作权限控制(按钮级)
  3. 实现基于角色的数据范围隔离
  4. 支持超级管理员全权限访问
  5. 新增运送清单创建功能并进行权限控制

二、功能需求详述

2.1 菜单权限管理

需求描述:

  • sys_menu 表维护菜单树与权限点
  • 后端根据用户 role_id 返回可见菜单列表
  • 前端动态生成路由,仅渲染可见菜单节点
  • 菜单权限调整后,用户重新获取菜单即可动态生效

涉及表:

  • sys_menu - 系统菜单表
  • sys_role_menu - 角色菜单关联表
  • sys_role - 角色表
  • sys_user - 用户表

2.2 操作权限管理

需求描述:

  • 基于权限码(permKey)控制页面按钮与接口访问
  • 前端通过 v-hasPermi/v-hasRole 指令控制按钮显示/禁用
  • 后端在 Controller 层使用 @SaCheckPermission 注解进行权限拦截
  • 防止前端绕过直接调用接口的越权操作

权限码示例:

  • delivery:list - 运送清单查看
  • delivery:add - 运送清单新增
  • delivery:edit - 运送清单编辑
  • delivery:delete - 运送清单删除
  • delivery:check - 运送清单核验
  • delivery:assign - 设备分配
  • delivery:load - 装车操作

2.3 数据范围隔离

需求描述:

  • 登录成功后读取 sys_user.role_id,作为数据作用域依据
  • 普通角色仅能访问本角色范围内的数据(创建人或核验人为当前用户)
  • 超级管理员可访问全部数据
  • 在 Service 层查询时动态添加数据权限过滤条件

2.4 超级管理员权限

需求描述:

  • 超级管理员具备最高权限与全数据可见性
  • 可以管理所有角色与菜单
  • 建议使用固定角色编码或 ID 标识(如 role_id = 1is_super_admin = 1

2.5 运送清单新增功能

需求描述:

  • 在运送清单列表页增加"新增"按钮
  • 仅具备 delivery:add 权限的用户可见与可操作
  • 打开表单弹窗,填写运送清单信息
  • 提交后调用后端接口创建记录,成功后刷新列表

表单字段:

  • 运输单号自动生成格式yyyyMMddHHmmss
  • 发货方(必填)
  • 采购方(必填)
  • 车牌号(必填,格式校验)
  • 司机姓名(必填)
  • 司机电话(必填,手机号校验)
  • 主机设备(下拉选择,可选)
  • 耳标设备(多选,可选)
  • 项圈设备(多选,可选)
  • 预计出发时间(必填,日期时间选择器)
  • 预计到达时间(必填,必须晚于出发时间)
  • 起点地址(必填)
  • 目的地地址(必填)
  • 牛只数量(必填,正整数)
  • 预估重量(公斤,必填,正数)
  • 检疫证号(可选)
  • 备注可选最多500字

三、后端开发方案

3.1 项目结构

c:/cattleTransport/tradeCattle/
├── aiotagro-core/                          # 核心模块
│   └── src/main/java/com/aiotagro/common/core/
│       ├── constant/
│       │   └── RoleConstants.java          # 【新增】角色常量
│       └── utils/
│           ├── SecurityUtil.java           # 【修改】安全工具类
│           └── DataScopeUtil.java          # 【新增】数据权限工具类
└── aiotagro-cattle-trade/                  # 业务模块
    └── src/main/java/com/aiotagro/cattletrade/
        ├── config/
        │   └── SaTokenConfigure.java       # 【修改】Sa-Token配置
        └── business/
            ├── controller/
            │   └── DeliveryController.java # 【修改】运送清单控制器
            ├── service/
            │   ├── LoginService.java
            │   └── impl/
            │       ├── LoginServiceImpl.java    # 【修改】登录服务实现
            │       └── DeliveryServiceImpl.java # 【修改】运送清单服务实现
            ├── dto/
            │   └── DeliveryCreateDto.java  # 【新增】运送清单创建DTO
            └── entity/
                ├── SysUser.java
                ├── SysRole.java
                └── SysMenu.java

3.2 详细实施步骤

步骤1创建角色常量类

文件: aiotagro-core/src/main/java/com/aiotagro/common/core/constant/RoleConstants.java

package com.aiotagro.common.core.constant;

/**
 * 角色相关常量
 */
public class RoleConstants {
    
    /**
     * 超级管理员角色ID
     */
    public static final Integer SUPER_ADMIN_ROLE_ID = 1;
    
    /**
     * 超级管理员角色编码
     */
    public static final String SUPER_ADMIN_ROLE_CODE = "ROLE_SUPER_ADMIN";
    
    /**
     * 所有权限标识
     */
    public static final String ALL_PERMISSION = "*:*:*";
}

步骤2扩展SecurityUtil工具类

文件: aiotagro-core/src/main/java/com/aiotagro/common/core/utils/SecurityUtil.java

修改内容: 添加以下方法

/**
 * 获取当前用户角色ID
 */
public static Integer getRoleId() {
    Object roleId = StpUtil.getTokenSession().get("roleId");
    return roleId != null ? Integer.parseInt(roleId.toString()) : null;
}

/**
 * 获取当前用户权限列表
 */
@SuppressWarnings("unchecked")
public static List<String> getPermissions() {
    Object permissions = StpUtil.getTokenSession().get("permissions");
    return permissions != null ? (List<String>) permissions : Collections.emptyList();
}

/**
 * 判断是否超级管理员
 */
public static boolean isSuperAdmin() {
    Integer roleId = getRoleId();
    return roleId != null && roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID);
}

/**
 * 判断是否拥有某个权限
 */
public static boolean hasPermission(String permission) {
    if (isSuperAdmin()) {
        return true;
    }
    List<String> permissions = getPermissions();
    return permissions.contains(RoleConstants.ALL_PERMISSION) || 
           permissions.contains(permission);
}

步骤3创建数据权限工具类

文件: aiotagro-core/src/main/java/com/aiotagro/common/core/utils/DataScopeUtil.java

package com.aiotagro.common.core.utils;

import com.aiotagro.common.core.constant.RoleConstants;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

/**
 * 数据权限工具类
 */
public class DataScopeUtil {
    
    /**
     * 应用数据权限过滤(基于创建人)
     * 超级管理员不过滤,普通用户只能看自己创建的数据
     */
    public static <T> void applyDataScope(LambdaQueryWrapper<T> wrapper, 
                                          SFunction<T, Integer> createdByField) {
        // 超级管理员不过滤
        if (SecurityUtil.isSuperAdmin()) {
            return;
        }
        
        // 普通用户只能查看自己创建的数据
        Integer currentUserId = SecurityUtil.getCurrentUserId();
        wrapper.eq(createdByField, currentUserId);
    }
    
    /**
     * 应用数据权限过滤(基于创建人或核验人)
     * 用户可以看到自己创建或自己核验的数据
     */
    public static <T> void applyDataScopeWithChecker(LambdaQueryWrapper<T> wrapper,
                                                     SFunction<T, Integer> createdByField,
                                                     SFunction<T, Integer> checkByField) {
        // 超级管理员不过滤
        if (SecurityUtil.isSuperAdmin()) {
            return;
        }
        
        // 普通用户可以查看自己创建或自己核验的数据
        Integer currentUserId = SecurityUtil.getCurrentUserId();
        wrapper.and(w -> w.eq(createdByField, currentUserId)
                         .or()
                         .eq(checkByField, currentUserId));
    }
}

步骤4修改登录服务实现

文件: aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/LoginServiceImpl.java

修改 login 方法第100行后 添加权限信息存储

// 原有代码
user.setLastLoginTime(new Date());
userMapper.updateById(user);

// 登录并设置用户信息到session
StpUtil.login(user.getId(), SaLoginConfig
        .setExtra("username", user.getName())
        .setExtra("mobile", user.getMobile())
        .setExtra("userType", user.getUserType()));

// ===== 【新增代码开始】 =====
// 存储角色ID到session
StpUtil.getTokenSession().set("roleId", user.getRoleId());

// 查询用户权限列表
List<String> permissions = queryUserPermissions(user.getRoleId());
StpUtil.getTokenSession().set("permissions", permissions);

// 查询用户角色信息
SysRole role = roleMapper.selectById(user.getRoleId());
String roleCode = role != null ? role.getCode() : "";
StpUtil.getTokenSession().set("roleCode", roleCode);
// ===== 【新增代码结束】 =====

Map<String, Object> result = new HashMap<>();
result.put("token", StpUtil.getTokenValue());
result.put("mobile", user.getMobile());
result.put("username", user.getName());
result.put("userType", user.getUserType());
result.put("roleId", user.getRoleId());  // 【新增】返回角色ID
result.put("permissions", permissions);   // 【新增】返回权限列表
return AjaxResult.success(result);

添加查询权限列表的方法:

/**
 * 查询用户权限列表
 */
private List<String> queryUserPermissions(Integer roleId) {
    if (roleId == null) {
        return Collections.emptyList();
    }
    
    // 如果是超级管理员,返回所有权限
    if (roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID)) {
        return Collections.singletonList(RoleConstants.ALL_PERMISSION);
    }
    
    // 查询角色关联的菜单权限
    List<SysMenu> menus = menuMapper.selectMenusByRoleId(roleId);
    return menus.stream()
            .filter(menu -> StringUtils.isNotEmpty(menu.getAuthority()))
            .map(SysMenu::getAuthority)
            .distinct()
            .collect(Collectors.toList());
}

在 SysMenuMapper 中添加方法:

/**
 * 根据角色ID查询菜单列表
 */
List<SysMenu> selectMenusByRoleId(@Param("roleId") Integer roleId);

在 SysMenuMapper.xml 中添加SQL

<select id="selectMenusByRoleId" resultType="com.aiotagro.cattletrade.business.entity.SysMenu">
    SELECT DISTINCT m.*
    FROM sys_menu m
    INNER JOIN sys_role_menu rm ON m.id = rm.menu_id
    WHERE rm.role_id = #{roleId}
      AND m.is_delete = 0
    ORDER BY m.sort ASC
</select>

步骤5修改DeliveryController添加权限注解

文件: aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/controller/DeliveryController.java

修改内容: 在类和方法上添加权限注解

package com.aiotagro.cattletrade.business.controller;

import cn.dev33.satoken.annotation.SaCheckPermission;  // 【新增】导入
import com.aiotagro.cattletrade.business.dto.*;
import com.aiotagro.cattletrade.business.entity.Delivery;
import com.aiotagro.cattletrade.business.service.IDeliveryService;
import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * 运送清单表 前端控制器
 */
@RestController
@RequestMapping("/delivery")
public class DeliveryController {

    @Autowired
    private IDeliveryService deliveryService;

    /**
     * 运送清单-分页查询
     */
    @SaCheckPermission("delivery:list")  // 【新增】
    @PostMapping(value = "/pageQuery")
    public PageResultResponse<Delivery> pageQuery(@RequestBody DeliveryQueryDto dto) {
        return deliveryService.pageQuery(dto);
    }

    /**
     * 运送清单-新增原有的add方法
     */
    @SaCheckPermission("delivery:add")  // 【新增】
    @PostMapping(value = "/add")
    public AjaxResult add(@RequestBody DeliveryAddDto dto) {
        try{
           return deliveryService.addDelivery(dto);
        }catch (Exception e){
            e.printStackTrace();
            return AjaxResult.error(e.getMessage());
        }
    }
    
    /**
     * 运送清单-创建新增的create接口
     */
    @SaCheckPermission("delivery:add")  // 【新增】
    @PostMapping(value = "/create")
    public AjaxResult create(@Validated @RequestBody DeliveryCreateDto dto) {
        try{
           return deliveryService.createDelivery(dto);
        }catch (Exception e){
            e.printStackTrace();
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 运送清单-查询详情
     */
    @SaCheckPermission("delivery:view")  // 【新增】
    @GetMapping(value = "/view")
    public AjaxResult view(@RequestParam Integer id) {
        return deliveryService.viewDelivery(id);
    }

    /**
     * 运送清单-核验提交
     */
    @SaCheckPermission("delivery:check")  // 【新增】
    @PostMapping(value = "/submitCheck")
    public AjaxResult submitCheck(@RequestBody Delivery delivery) {
        // ... 原有代码
    }
    
    /**
     * 运送清单-编辑
     */
    @SaCheckPermission("delivery:edit")  // 【新增】
    @PostMapping(value = "/update")
    public AjaxResult update(@RequestBody Delivery delivery) {
        // ... 原有代码
    }
    
    /**
     * 运送清单-删除
     */
    @SaCheckPermission("delivery:delete")  // 【新增】
    @GetMapping(value = "/delete")
    public AjaxResult delete(@RequestParam Integer id) {
        // ... 原有代码
    }
}

步骤6创建运送清单创建DTO

文件: aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/dto/DeliveryCreateDto.java

package com.aiotagro.cattletrade.business.dto;

import lombok.Data;
import javax.validation.constraints.*;
import java.util.Date;
import java.util.List;

/**
 * 运送清单创建DTO
 */
@Data
public class DeliveryCreateDto {
    
    /**
     * 发货方
     */
    @NotBlank(message = "发货方不能为空")
    private String shipper;
    
    /**
     * 采购方
     */
    @NotBlank(message = "采购方不能为空")
    private String buyer;
    
    /**
     * 车牌号
     */
    @NotBlank(message = "车牌号不能为空")
    @Pattern(regexp = "^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{5}[A-Z0-9挂学警港澳]$", 
             message = "车牌号格式不正确")
    private String plateNumber;
    
    /**
     * 司机姓名
     */
    @NotBlank(message = "司机姓名不能为空")
    private String driverName;
    
    /**
     * 司机电话
     */
    @NotBlank(message = "司机电话不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String driverPhone;
    
    /**
     * 主机设备ID
     */
    private Integer serverId;
    
    /**
     * 耳标设备ID列表
     */
    private List<Integer> eartagIds;
    
    /**
     * 项圈设备ID列表
     */
    private List<Integer> collarIds;
    
    /**
     * 预计出发时间
     */
    @NotNull(message = "预计出发时间不能为空")
    private Date estimatedDepartureTime;
    
    /**
     * 预计到达时间
     */
    @NotNull(message = "预计到达时间不能为空")
    private Date estimatedArrivalTime;
    
    /**
     * 起点地址
     */
    @NotBlank(message = "起点地址不能为空")
    private String startLocation;
    
    /**
     * 目的地地址
     */
    @NotBlank(message = "目的地地址不能为空")
    private String endLocation;
    
    /**
     * 牛只数量
     */
    @NotNull(message = "牛只数量不能为空")
    @Min(value = 1, message = "牛只数量至少为1")
    private Integer cattleCount;
    
    /**
     * 预估重量(公斤)
     */
    @NotNull(message = "预估重量不能为空")
    @DecimalMin(value = "0.01", message = "预估重量必须大于0")
    private Double estimatedWeight;
    
    /**
     * 检疫证号
     */
    private String quarantineCertNo;
    
    /**
     * 备注
     */
    @Size(max = 500, message = "备注不能超过500字")
    private String remark;
}

步骤7在DeliveryService中添加创建方法

接口文件: IDeliveryService.java

/**
 * 创建运送清单
 */
AjaxResult createDelivery(DeliveryCreateDto dto);

实现文件: DeliveryServiceImpl.java

@Transactional
@Override
public AjaxResult createDelivery(DeliveryCreateDto dto) {
    // 校验时间逻辑
    if (dto.getEstimatedArrivalTime().before(dto.getEstimatedDepartureTime())) {
        return AjaxResult.error("预计到达时间必须晚于出发时间");
    }
    
    // 获取当前登录用户
    Integer userId = SecurityUtil.getCurrentUserId();
    String userName = SecurityUtil.getUserName();
    
    // 生成运输单号
    LocalDateTime now = LocalDateTime.now();
    String deliveryNumber = "YS" + now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
    
    // 创建运送清单
    Delivery delivery = new Delivery();
    delivery.setDeliveryNumber(deliveryNumber);
    delivery.setShipper(dto.getShipper());
    delivery.setBuyer(dto.getBuyer());
    delivery.setPlateNumber(dto.getPlateNumber());
    delivery.setDriverName(dto.getDriverName());
    delivery.setDriverPhone(dto.getDriverPhone());
    delivery.setEstimatedDepartureTime(dto.getEstimatedDepartureTime());
    delivery.setEstimatedArrivalTime(dto.getEstimatedArrivalTime());
    delivery.setStartLocation(dto.getStartLocation());
    delivery.setEndLocation(dto.getEndLocation());
    delivery.setCattleCount(dto.getCattleCount());
    delivery.setEstimatedWeight(dto.getEstimatedWeight());
    delivery.setQuarantineCertNo(dto.getQuarantineCertNo());
    delivery.setRemark(dto.getRemark());
    delivery.setStatus(1); // 待装车
    delivery.setCreatedBy(userId);
    delivery.setCreatedByName(userName);
    delivery.setCreateTime(new Date());
    
    // 保存运送清单
    this.save(delivery);
    
    // 如果选择了设备,保存设备关联
    if (dto.getServerId() != null || 
        CollectionUtils.isNotEmpty(dto.getEartagIds()) || 
        CollectionUtils.isNotEmpty(dto.getCollarIds())) {
        // 保存设备关联逻辑(根据实际业务调整)
        saveDeliveryDevices(delivery.getId(), dto);
    }
    
    return AjaxResult.success("创建成功", delivery);
}

/**
 * 保存运送清单设备关联
 */
private void saveDeliveryDevices(Integer deliveryId, DeliveryCreateDto dto) {
    // 根据实际业务实现设备关联逻辑
    // 示例代码,需要根据实际的 DeliveryDevice 表结构调整
}

步骤8修改DeliveryServiceImpl的查询方法

文件: DeliveryServiceImpl.java

修改 pageQuery 方法: 应用数据权限过滤

@Override
public PageResultResponse<Delivery> pageQuery(DeliveryQueryDto dto) {
    // 获取当前登录人id
    Integer userId = SecurityUtil.getCurrentUserId();
    Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
    
    LambdaQueryWrapper<Delivery> wrapper = new LambdaQueryWrapper<>();
    
    // ===== 【修改:应用数据权限过滤】 =====
    if (!SecurityUtil.isSuperAdmin()) {
        // 普通用户只能查看自己创建或自己核验的数据
        wrapper.and(w -> w.eq(Delivery::getCreatedBy, userId)
                         .or()
                         .eq(Delivery::getCheckBy, userId));
    }
    // 否则超级管理员可以看到所有数据
    // ===== 【修改结束】 =====
    
    // 运输单号模糊查询
    if(StringUtils.isNotEmpty(dto.getDeliveryNumber())){
        wrapper.like(Delivery::getDeliveryNumber, dto.getDeliveryNumber());
    }
    
    wrapper.orderByDesc(Delivery::getId);
    List<Delivery> list = this.list(wrapper);
    
    if(CollectionUtils.isNotEmpty(list)){
        list.forEach(delivery -> {
            if(userId.equals(delivery.getCheckBy())){
                // 判断是否需要核验1需要核验2不需要核验
                delivery.setIfCheck(1);
            }
        });
    }
    
    return new PageResultResponse(result.getTotal(), list);
}

步骤9修改Sa-Token配置可选

文件: SaTokenConfigure.java

如果需要排除某些路径不进行权限校验,可以修改:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor(handler -> StpUtil.checkLogin()))
            .addPathPatterns("/**")
            .excludePathPatterns(
                "/login",                // 登录
                "/sendLoginSmsCode",     // 发送验证码
                "/common/upload",        // 文件上传
                "/warningLog/savaWarn"  // 告警保存
            );
}

四、前端开发方案

4.1 项目结构

pc-cattle-transportation/
├── src/
│   ├── api/
│   │   └── shipping.js                     # 【修改】运送清单API
│   ├── components/
│   │   └── common/
│   ├── directive/
│   │   └── permission/
│   │       └── hasPermi.js                 # 权限指令
│   ├── store/
│   │   ├── user.ts                         # 【修改】用户状态管理
│   │   └── permission.js                   # 权限状态管理
│   ├── utils/
│   │   ├── permission.js                   # 权限工具函数
│   │   └── validate.js                     # 【修改】表单校验规则
│   └── views/
│       └── shipping/
│           ├── loadingOrder.vue            # 【修改】运送清单列表
│           └── createDeliveryDialog.vue    # 【新增】运送清单创建弹窗

4.2 详细实施步骤

步骤1扩展用户Store

文件: pc-cattle-transportation/src/store/user.ts

import { defineStore } from 'pinia';

export const useUserStore = defineStore('storeUser', {
    state: () => {
        return {
            id: '',
            username: '',
            token: '',
            loginUser: null,
            userType: '',
            roleId: '',
            permissions: [] as string[],  // 【新增】权限列表
            roles: [] as string[],         // 【新增】角色列表
        };
    },
    persist: {
        enabled: true,
        strategies: [
            {
                key: 'userStore',
                storage: localStorage,
            },
        ],
    },
    getters: {
        // 【新增】判断是否有某个权限
        hasPermission: (state) => {
            return (permission: string) => {
                if (state.permissions.includes('*:*:*')) {
                    return true;
                }
                return state.permissions.includes(permission);
            };
        },
        // 【新增】判断是否有某个角色
        hasRole: (state) => {
            return (role: string) => {
                if (state.roles.includes('admin')) {
                    return true;
                }
                return state.roles.includes(role);
            };
        },
    },
    actions: {
        removeAllInfo() {
            this.id = '';
            this.username = '';
            this.token = '';
            this.userType = '';
            this.loginUser = null;
            this.roleId = '';
            this.permissions = [];  // 【新增】
            this.roles = [];        // 【新增】
        },
        updateToken(token: string) {
            this.token = token;
        },
        updateUserType(userType: string) {
            this.userType = userType;
        },
        updateUserName(userName: string) {
            this.username = userName;
        },
        updateLoginUser(user: Object) {
            // @ts-ignore
            this.loginUser = user;
        },
        updateLoginUserType(userType: number) {
            // @ts-ignore
            this.userType = userType;
        },
        updateRoleId(roleId: string) {
            this.roleId = roleId;
        },
        // 【新增】更新权限列表
        updatePermissions(permissions: string[]) {
            this.permissions = permissions || [];
        },
        // 【新增】更新角色列表
        updateRoles(roles: string[]) {
            this.roles = roles || [];
        },
    },
});

步骤2修改登录页面

文件: pc-cattle-transportation/src/views/login.vue

在登录成功的处理逻辑中添加:

// 找到登录成功的处理代码,通常在 handleLogin 或类似方法中
const handleLogin = async () => {
    try {
        const res = await login(loginForm);
        if (res.code === 200) {
            const { token, username, userType, roleId, permissions, roles } = res.data;
            
            // 保存用户信息
            userStore.updateToken(token);
            userStore.updateUserName(username);
            userStore.updateUserType(userType);
            userStore.updateRoleId(roleId);
            userStore.updatePermissions(permissions || []);  // 【新增】
            userStore.updateRoles(roles || []);              // 【新增】
            
            // 跳转到首页
            router.push('/');
        } else {
            ElMessage.error(res.msg || '登录失败');
        }
    } catch (error) {
        ElMessage.error('登录失败,请稍后重试');
    }
};

步骤3添加运送清单创建API

文件: pc-cattle-transportation/src/api/shipping.js

// 在文件末尾添加
// 运送清单 - 创建
export function createDelivery(data) {
    return request({
        url: '/delivery/create',
        method: 'POST',
        data,
    });
}

// 查询可用主机设备列表
export function getAvailableServers(data) {
    return request({
        url: '/jbqServer/availableList',
        method: 'POST',
        data,
    });
}

// 查询可用耳标设备列表
export function getAvailableEartags(data) {
    return request({
        url: '/jbqClient/availableList',
        method: 'POST',
        data,
    });
}

// 查询可用项圈设备列表
export function getAvailableCollars(data) {
    return request({
        url: '/xqClient/availableList',
        method: 'POST',
        data,
    });
}

步骤4创建运送清单创建弹窗组件

文件: pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue

<template>
    <el-dialog
        v-model="dialogVisible"
        title="新增运送清单"
        width="800px"
        :close-on-click-modal="false"
        @close="handleClose"
    >
        <el-form
            ref="formRef"
            :model="formData"
            :rules="rules"
            label-width="120px"
        >
            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="发货方" prop="shipper">
                        <el-input v-model="formData.shipper" placeholder="请输入发货方" />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="采购方" prop="buyer">
                        <el-input v-model="formData.buyer" placeholder="请输入采购方" />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="车牌号" prop="plateNumber">
                        <el-input v-model="formData.plateNumber" placeholder="如京A12345" />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="司机姓名" prop="driverName">
                        <el-input v-model="formData.driverName" placeholder="请输入司机姓名" />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="司机电话" prop="driverPhone">
                        <el-input v-model="formData.driverPhone" placeholder="请输入司机电话" />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="主机设备" prop="serverId">
                        <el-select
                            v-model="formData.serverId"
                            placeholder="请选择主机设备"
                            clearable
                            filterable
                            style="width: 100%"
                        >
                            <el-option
                                v-for="item in serverList"
                                :key="item.id"
                                :label="item.deviceNo"
                                :value="item.id"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="耳标设备" prop="eartagIds">
                        <el-select
                            v-model="formData.eartagIds"
                            placeholder="请选择耳标设备"
                            multiple
                            clearable
                            filterable
                            style="width: 100%"
                        >
                            <el-option
                                v-for="item in eartagList"
                                :key="item.id"
                                :label="item.deviceNo"
                                :value="item.id"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="项圈设备" prop="collarIds">
                        <el-select
                            v-model="formData.collarIds"
                            placeholder="请选择项圈设备"
                            multiple
                            clearable
                            filterable
                            style="width: 100%"
                        >
                            <el-option
                                v-for="item in collarList"
                                :key="item.id"
                                :label="item.deviceNo"
                                :value="item.id"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="预计出发时间" prop="estimatedDepartureTime">
                        <el-date-picker
                            v-model="formData.estimatedDepartureTime"
                            type="datetime"
                            placeholder="请选择预计出发时间"
                            style="width: 100%"
                            value-format="YYYY-MM-DD HH:mm:ss"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="预计到达时间" prop="estimatedArrivalTime">
                        <el-date-picker
                            v-model="formData.estimatedArrivalTime"
                            type="datetime"
                            placeholder="请选择预计到达时间"
                            style="width: 100%"
                            value-format="YYYY-MM-DD HH:mm:ss"
                        />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="起点地址" prop="startLocation">
                        <el-input v-model="formData.startLocation" placeholder="请输入起点地址" />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="目的地地址" prop="endLocation">
                        <el-input v-model="formData.endLocation" placeholder="请输入目的地地址" />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="牛只数量" prop="cattleCount">
                        <el-input-number
                            v-model="formData.cattleCount"
                            :min="1"
                            :max="9999"
                            placeholder="请输入牛只数量"
                            style="width: 100%"
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="预估重量(kg)" prop="estimatedWeight">
                        <el-input-number
                            v-model="formData.estimatedWeight"
                            :min="0.01"
                            :precision="2"
                            placeholder="请输入预估重量"
                            style="width: 100%"
                        />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="12">
                    <el-form-item label="检疫证号" prop="quarantineCertNo">
                        <el-input v-model="formData.quarantineCertNo" placeholder="请输入检疫证号(可选)" />
                    </el-form-item>
                </el-col>
            </el-row>

            <el-form-item label="备注" prop="remark">
                <el-input
                    v-model="formData.remark"
                    type="textarea"
                    :rows="3"
                    maxlength="500"
                    show-word-limit
                    placeholder="请输入备注信息(可选)"
                />
            </el-form-item>
        </el-form>

        <template #footer>
            <span class="dialog-footer">
                <el-button @click="handleClose">取消</el-button>
                <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
                    提交
                </el-button>
            </span>
        </template>
    </el-dialog>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { createDelivery, getAvailableServers, getAvailableEartags, getAvailableCollars } from '@/api/shipping.js';

const dialogVisible = ref(false);
const formRef = ref(null);
const submitLoading = ref(false);
const serverList = ref([]);
const eartagList = ref([]);
const collarList = ref([]);

const formData = reactive({
    shipper: '',
    buyer: '',
    plateNumber: '',
    driverName: '',
    driverPhone: '',
    serverId: null,
    eartagIds: [],
    collarIds: [],
    estimatedDepartureTime: '',
    estimatedArrivalTime: '',
    startLocation: '',
    endLocation: '',
    cattleCount: 1,
    estimatedWeight: null,
    quarantineCertNo: '',
    remark: '',
});

// 车牌号校验
const validatePlateNumber = (rule, value, callback) => {
    const plateReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{5}[A-Z0-9挂学警港澳]$/;
    if (!value) {
        callback(new Error('请输入车牌号'));
    } else if (!plateReg.test(value)) {
        callback(new Error('车牌号格式不正确'));
    } else {
        callback();
    }
};

// 手机号校验
const validatePhone = (rule, value, callback) => {
    const phoneReg = /^1[3-9]\d{9}$/;
    if (!value) {
        callback(new Error('请输入司机电话'));
    } else if (!phoneReg.test(value)) {
        callback(new Error('手机号格式不正确'));
    } else {
        callback();
    }
};

// 时间校验
const validateArrivalTime = (rule, value, callback) => {
    if (!value) {
        callback(new Error('请选择预计到达时间'));
    } else if (formData.estimatedDepartureTime && value <= formData.estimatedDepartureTime) {
        callback(new Error('预计到达时间必须晚于出发时间'));
    } else {
        callback();
    }
};

const rules = {
    shipper: [{ required: true, message: '请输入发货方', trigger: 'blur' }],
    buyer: [{ required: true, message: '请输入采购方', trigger: 'blur' }],
    plateNumber: [{ required: true, validator: validatePlateNumber, trigger: 'blur' }],
    driverName: [{ required: true, message: '请输入司机姓名', trigger: 'blur' }],
    driverPhone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
    estimatedDepartureTime: [{ required: true, message: '请选择预计出发时间', trigger: 'change' }],
    estimatedArrivalTime: [{ required: true, validator: validateArrivalTime, trigger: 'change' }],
    startLocation: [{ required: true, message: '请输入起点地址', trigger: 'blur' }],
    endLocation: [{ required: true, message: '请输入目的地地址', trigger: 'blur' }],
    cattleCount: [{ required: true, message: '请输入牛只数量', trigger: 'blur' }],
    estimatedWeight: [{ required: true, message: '请输入预估重量', trigger: 'blur' }],
};

// 打开弹窗
const open = () => {
    dialogVisible.value = true;
    loadDeviceOptions();
};

// 加载设备选项
const loadDeviceOptions = async () => {
    try {
        // 加载主机设备
        const serverRes = await getAvailableServers({ pageNum: 1, pageSize: 9999, status: 1 });
        if (serverRes.code === 200) {
            serverList.value = serverRes.data.rows || [];
        }

        // 加载耳标设备
        const eartagRes = await getAvailableEartags({ pageNum: 1, pageSize: 9999, status: 1 });
        if (eartagRes.code === 200) {
            eartagList.value = eartagRes.data.rows || [];
        }

        // 加载项圈设备
        const collarRes = await getAvailableCollars({ pageNum: 1, pageSize: 9999, status: 1 });
        if (collarRes.code === 200) {
            collarList.value = collarRes.data.rows || [];
        }
    } catch (error) {
        console.error('加载设备列表失败', error);
    }
};

// 提交表单
const handleSubmit = () => {
    formRef.value.validate(async (valid) => {
        if (valid) {
            submitLoading.value = true;
            try {
                const res = await createDelivery(formData);
                if (res.code === 200) {
                    ElMessage.success('创建成功');
                    dialogVisible.value = false;
                    emit('success');
                } else {
                    ElMessage.error(res.msg || '创建失败');
                }
            } catch (error) {
                ElMessage.error('创建失败,请稍后重试');
            } finally {
                submitLoading.value = false;
            }
        }
    });
};

// 关闭弹窗
const handleClose = () => {
    formRef.value?.resetFields();
    dialogVisible.value = false;
};

// 暴露方法给父组件
defineExpose({
    open,
});

const emit = defineEmits(['success']);
</script>

<style scoped>
.dialog-footer {
    display: flex;
    justify-content: flex-end;
}
</style>

步骤5修改运送清单列表页

文件: pc-cattle-transportation/src/views/shipping/loadingOrder.vue

在模板部分,修改按钮区域:

<template>
    <div>
        <base-search :formItemList="formItemList" @search="searchFrom" ref="baseSearchRef"> </base-search>
        <div style="display: flex; padding: 10px; background: #fff; margin-bottom: 10px">
            <!-- 原有按钮 -->
            <el-button type="primary" @click="showAddDialog(null)">创建装车订单</el-button>
            
            <!-- 新增新增运送清单按钮带权限控制 -->
            <el-button 
                type="primary" 
                v-hasPermi="['delivery:add']" 
                @click="showCreateDeliveryDialog"
            >
                新增运送清单
            </el-button>
        </div>
        
        <div class="main-container">
            <el-table :data="data.rows" border v-loading="data.dataListLoading" element-loading-text="数据加载中..." style="width: 100%">
                <!-- ... 表格列定义 ... -->
                
                <el-table-column label="操作" width="360">
                    <template #default="scope">
                        <!-- 修改在各个操作按钮上添加权限控制 -->
                        <el-button 
                            link 
                            type="primary" 
                            :disabled="scope.row.status != 1" 
                            v-hasPermi="['delivery:edit']"
                            @click="showEditDialog(scope.row)"
                        >
                            编辑
                        </el-button>
                        <el-button 
                            link 
                            type="primary" 
                            :disabled="scope.row.status != 1" 
                            v-hasPermi="['delivery:assign']"
                            @click="showAssignDialog(scope.row)"
                        >
                            分配设备
                        </el-button>
                        <el-button 
                            link 
                            type="primary" 
                            v-hasPermi="['delivery:view']"
                            @click="showDetailDialog(scope.row)"
                        >
                            详情
                        </el-button>
                        <el-button 
                            link 
                            type="primary" 
                            v-hasPermi="['delivery:view']"
                            @click="showLookDialog(scope.row)"
                        >
                            查看设备
                        </el-button>
                        <el-button 
                            link 
                            type="primary" 
                            :disabled="scope.row.status != 1" 
                            v-hasPermi="['delivery:delete']"
                            @click="del(scope.row.id)"
                        >
                            删除
                        </el-button>
                        <el-button 
                            link 
                            type="primary" 
                            :disabled="scope.row.status != 1" 
                            v-hasPermi="['delivery:load']"
                            @click="loadClick(scope.row)"
                        >
                            装车
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>
            
            <pagination v-model:limit="form.pageSize" v-model:page="form.pageNum" :total="data.total" @pagination="getDataList" />
            
            <!-- 原有对话框组件 -->
            <OrderDialog ref="OrderDialogRef" @success="getDataList" />
            <LookDialog ref="LookDialogRef" />
            <AssignDialog ref="AssignDialogRef" @success="getDataList" />
            <DetailDialog ref="DetailDialogRef" />
            <editDialog ref="editDialogRef" @success="getDataList" />
            <LoadDialog ref="LoadDialogRef" @success="getDataList" />
            
            <!-- 新增运送清单创建对话框 -->
            <CreateDeliveryDialog ref="CreateDeliveryDialogRef" @success="getDataList" />
        </div>
    </div>
</template>

在 script 部分,添加导入和方法:

import CreateDeliveryDialog from './createDeliveryDialog.vue';  // 【新增】

const CreateDeliveryDialogRef = ref(null);  // 【新增】

// 【新增】显示创建运送清单对话框
const showCreateDeliveryDialog = () => {
    CreateDeliveryDialogRef.value.open();
};

步骤6确保权限指令已全局注册

文件: pc-cattle-transportation/src/directive/index.js

确保包含以下内容:

import hasPermi from './permission/hasPermi';
import hasRole from './permission/hasRole';  // 如果有角色指令

export default function directive(app) {
    app.directive('hasPermi', hasPermi);
    app.directive('hasRole', hasRole);  // 如果有角色指令
}

main.ts 中确保已引入:

import directive from './directive';

const app = createApp(App);
directive(app);  // 注册自定义指令

五、数据库变更

5.1 可选:添加超级管理员标识字段

-- 在 sys_role 表添加超级管理员标识字段(可选)
ALTER TABLE `sys_role` 
ADD COLUMN `is_super_admin` TINYINT(1) DEFAULT 0 COMMENT '是否超级管理员 1-是 0-否' AFTER `description`;

-- 设置某个角色为超级管理员
UPDATE `sys_role` SET `is_super_admin` = 1 WHERE `id` = 1;

5.2 确保权限点数据完整

sys_menu 表中添加运送清单相关权限点(如果不存在):

-- 运送清单菜单假设父级菜单ID为2
INSERT INTO `sys_menu` (`parent_id`, `type`, `name`, `route_url`, `page_url`, `authority`, `icon`, `sort`) 
VALUES (2, 1, '运送清单', '/shipping/list', 'shipping/loadingOrder', 'delivery:list', 'icon-shipping', 10);

-- 获取刚插入的菜单ID假设为100
SET @menu_id = LAST_INSERT_ID();

-- 运送清单按钮权限
INSERT INTO `sys_menu` (`parent_id`, `type`, `name`, `authority`, `sort`) VALUES
(@menu_id, 2, '运送清单新增', 'delivery:add', 1),
(@menu_id, 2, '运送清单编辑', 'delivery:edit', 2),
(@menu_id, 2, '运送清单删除', 'delivery:delete', 3),
(@menu_id, 2, '运送清单查看', 'delivery:view', 4),
(@menu_id, 2, '运送清单核验', 'delivery:check', 5),
(@menu_id, 2, '设备分配', 'delivery:assign', 6),
(@menu_id, 2, '装车操作', 'delivery:load', 7);

5.3 为角色分配权限

-- 为某个角色假设role_id=2分配运送清单权限
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 2, id FROM `sys_menu` WHERE `authority` LIKE 'delivery:%';

六、测试计划

6.1 权限测试

6.1.1 菜单权限测试

  1. 测试场景: 不同角色登录系统
  2. 测试步骤:
    • 创建2个角色超级管理员、普通操作员
    • 为普通操作员分配部分菜单权限(不包含某些菜单)
    • 分别用两个角色的账号登录
  3. 预期结果:
    • 超级管理员能看到所有菜单
    • 普通操作员只能看到被分配的菜单
    • 未分配的菜单在侧边栏不显示

6.1.2 按钮权限测试

  1. 测试场景: 运送清单列表页按钮权限
  2. 测试步骤:
    • 创建角色Adelivery:listdelivery:view 权限
    • 创建角色Bdelivery:listdelivery:viewdelivery:adddelivery:edit 权限
    • 分别用两个角色的账号登录,访问运送清单页面
  3. 预期结果:
    • 角色A用户能看到"详情"、"查看设备"按钮,看不到"新增"、"编辑"、"删除"按钮
    • 角色B用户能看到"新增"、"编辑"、"详情"按钮

6.1.3 接口权限测试

  1. 测试场景: 通过Postman直接调用接口
  2. 测试步骤:
    • 使用普通操作员tokendelivery:add 权限)
    • 直接调用 POST /api/delivery/create 接口
  3. 预期结果:
    • 接口返回权限不足错误403或自定义错误码
    • 数据库未创建记录

6.2 数据隔离测试

6.2.1 普通用户数据隔离

  1. 测试场景: 普通用户只能看到自己的数据
  2. 测试步骤:
    • 创建用户AroleId=2创建运送清单数据
    • 创建用户BroleId=3创建运送清单数据
    • 用户A登录查询运送清单列表
  3. 预期结果:
    • 用户A只能看到自己创建的运送清单
    • 看不到用户B创建的数据

6.2.2 超级管理员数据访问

  1. 测试场景: 超级管理员能看到所有数据
  2. 测试步骤:
    • 多个普通用户创建运送清单数据
    • 超级管理员登录查询运送清单列表
  3. 预期结果:
    • 超级管理员能看到所有用户创建的运送清单

6.3 运送清单新增功能测试

6.3.1 表单校验测试

  1. 必填项校验:
    • 不填写必填项,点击提交
    • 预期:表单提示相应字段不能为空
  2. 格式校验:
    • 输入不符合格式的车牌号ABC123
    • 输入不符合格式的手机号12345678
    • 预期:表单提示格式错误
  3. 逻辑校验:
    • 预计到达时间早于出发时间
    • 预期:表单提示时间逻辑错误

6.3.2 设备选择测试

  1. 测试场景: 选择不同类型设备
  2. 测试步骤:
    • 选择主机设备
    • 选择多个耳标设备
    • 选择多个项圈设备
    • 提交表单
  3. 预期结果:
    • 表单提交成功
    • 设备关联关系正确保存
    • 可以不选择任何设备(设备可选)

6.3.3 提交成功测试

  1. 测试场景: 正确填写所有必填项并提交
  2. 测试步骤:
    • 填写所有必填字段
    • 点击提交按钮
  3. 预期结果:
    • 弹窗关闭
    • 提示"创建成功"
    • 列表自动刷新,显示新创建的记录
    • 运输单号自动生成,格式正确

6.4 集成测试

6.4.1 完整流程测试

  1. 普通用户登录
  2. 查看运送清单列表(只显示自己的数据)
  3. 点击"新增运送清单"按钮(如有权限)
  4. 填写表单并提交
  5. 查看新创建的记录
  6. 编辑、删除操作(如有权限)
  7. 退出登录,切换其他用户,验证数据隔离

6.4.2 权限动态更新测试

  1. 管理员修改某个角色的菜单权限
  2. 该角色的用户重新登录(或刷新页面重新获取菜单)
  3. 验证菜单和按钮权限已更新

七、实施步骤与时间估算

7.1 开发阶段

阶段 任务 预计时间 责任人
后端开发
1 创建RoleConstants常量类 0.5小时 后端开发
2 扩展SecurityUtil工具类 1小时 后端开发
3 创建DataScopeUtil工具类 1.5小时 后端开发
4 修改LoginServiceImpl添加权限查询和存储 2小时 后端开发
5 创建DeliveryCreateDto 1小时 后端开发
6 DeliveryController添加权限注解 1小时 后端开发
7 DeliveryServiceImpl实现createDelivery方法 2小时 后端开发
8 DeliveryServiceImpl修改pageQuery应用数据权限 1小时 后端开发
9 后端单元测试 2小时 后端开发
前端开发
10 扩展User Store 1小时 前端开发
11 修改登录页面保存权限信息 0.5小时 前端开发
12 添加运送清单创建API 0.5小时 前端开发
13 创建createDeliveryDialog组件 4小时 前端开发
14 修改loadingOrder页面添加按钮和权限控制 2小时 前端开发
15 前端功能测试 2小时 前端开发
数据库
16 添加超级管理员标识字段(可选) 0.5小时 DBA
17 添加权限点数据 1小时 DBA
18 为角色分配权限 0.5小时 DBA

总计开发时间:约 24 小时3个工作日

7.2 测试阶段

阶段 任务 预计时间 责任人
1 权限测试(菜单、按钮、接口) 3小时 测试工程师
2 数据隔离测试 2小时 测试工程师
3 运送清单新增功能测试 2小时 测试工程师
4 集成测试 2小时 测试工程师
5 Bug修复 4小时 开发团队
6 回归测试 2小时 测试工程师

总计测试时间:约 15 小时2个工作日

7.3 部署上线

阶段 任务 预计时间 责任人
1 准备部署文档 1小时 开发团队
2 数据库脚本执行 0.5小时 DBA
3 后端代码部署 1小时 运维工程师
4 前端代码部署 0.5小时 运维工程师
5 生产环境验证测试 2小时 测试工程师

总计部署时间:约 5 小时0.5个工作日)


八、技术要点与注意事项

8.1 后端要点

  1. Sa-Token权限注解

    • @SaCheckPermission("xxx") - 检查权限
    • @SaCheckRole("xxx") - 检查角色
    • @SaIgnore - 忽略登录校验(用于登录接口)
  2. 数据权限过滤

    • 在Service层而非Controller层进行数据权限过滤
    • 超级管理员不进行数据过滤
    • 考虑使用MyBatis-Plus的拦截器实现更优雅的数据权限控制
  3. 异常处理

    • Sa-Token会抛出NotPermissionException,需要在全局异常处理器中捕获
    • 返回友好的错误提示给前端
  4. 性能优化

    • 权限查询结果可以缓存到Redis中
    • 避免每次请求都查询数据库

8.2 前端要点

  1. 权限指令使用

    • v-hasPermi="['xxx']" - 控制元素显示/隐藏
    • 支持数组形式,满足其一即可显示
    • 支持 v-hasPermi="['xxx', 'yyy']" 或运算
  2. 状态持久化

    • 使用 pinia-plugin-persistedstate 持久化用户信息和权限
    • 刷新页面后权限状态不丢失
  3. 路由守卫

    • 在路由守卫中检查token有效性
    • 动态加载路由时检查权限
  4. 表单校验

    • 使用Element Plus的表单校验
    • 自定义校验规则处理复杂逻辑
    • 前端校验为辅,后端校验为主

8.3 安全注意事项

  1. 防止权限绕过

    • 前端权限控制仅用于UI显示
    • 后端必须进行权限拦截
    • 所有接口都要有权限校验
  2. 数据隔离

    • 不能只依赖前端传参
    • 后端必须根据token中的用户信息进行数据过滤
  3. 日志审计

    • 记录权限变更操作
    • 记录敏感数据的访问日志
  4. Token安全

    • 设置合理的token过期时间
    • 考虑实现token自动续期
    • 重要操作需要二次验证

8.4 兼容性考虑

  1. 浏览器兼容性

    • 测试Chrome、Edge、Firefox等主流浏览器
    • 考虑IE11兼容性如需要
  2. 数据库兼容性

    • SQL语句避免使用数据库特有语法
    • 注意字符集和排序规则
  3. 版本升级

    • 保留向后兼容性
    • 考虑老版本客户端的处理

九、风险与应对措施

9.1 技术风险

风险 影响 概率 应对措施
Sa-Token权限注解失效 充分测试,准备手动权限校验方案
数据权限过滤遗漏 Code Review编写测试用例覆盖
前端权限状态丢失 使用持久化插件,添加错误处理
性能影响 权限信息缓存,数据库索引优化

9.2 业务风险

风险 影响 概率 应对措施
权限配置错误导致无法访问 提供超级管理员账号,保留后门
数据隔离影响现有业务流程 充分沟通业务需求,灰度发布
用户培训不足 准备操作手册,提供培训

9.3 进度风险

风险 影响 概率 应对措施
需求变更 预留缓冲时间,敏捷开发
测试问题较多 提前介入测试,增加测试资源
依赖环境问题 提前准备环境,做好备份

十、交付物清单

10.1 代码文件

后端:

  • RoleConstants.java - 角色常量类
  • SecurityUtil.java - 安全工具类(修改)
  • DataScopeUtil.java - 数据权限工具类
  • LoginServiceImpl.java - 登录服务实现(修改)
  • DeliveryController.java - 运送清单控制器(修改)
  • DeliveryCreateDto.java - 运送清单创建DTO
  • DeliveryServiceImpl.java - 运送清单服务实现(修改)
  • SysMenuMapper.java - 菜单Mapper修改
  • SysMenuMapper.xml - 菜单Mapper XML修改

前端:

  • user.ts - 用户状态管理(修改)
  • shipping.js - 运送清单API修改
  • createDeliveryDialog.vue - 运送清单创建弹窗(新增)
  • loadingOrder.vue - 运送清单列表页(修改)

10.2 数据库脚本

  • permission_init.sql - 权限初始化脚本
  • alter_table.sql - 表结构变更脚本(可选)

10.3 文档

  • 权限管理和运送清单新增功能实施计划.md - 本文档
  • 部署文档.md - 部署步骤说明
  • 测试报告.md - 测试结果报告
  • 用户操作手册.md - 最终用户使用指南

10.4 测试用例

  • 权限测试用例Excel
  • 数据隔离测试用例Excel
  • 运送清单新增功能测试用例Excel

十一、后续优化建议

11.1 功能增强

  1. 细粒度数据权限

    • 支持部门级、区域级数据权限
    • 支持自定义数据权限规则
  2. 权限管理界面

    • 可视化权限配置界面
    • 权限模板快速应用
  3. 审计日志

    • 权限变更日志
    • 敏感操作日志
    • 日志查询和导出

11.2 性能优化

  1. 权限缓存优化

    • Redis缓存用户权限
    • 缓存失效策略
  2. 数据权限优化

    • MyBatis-Plus数据权限插件
    • SQL优化和索引优化
  3. 前端性能优化

    • 权限指令懒加载
    • 虚拟滚动优化大列表

11.3 安全加固

  1. 双因素认证

    • 敏感操作需要短信验证码
    • 登录需要图形验证码
  2. 会话管理

    • 异常登录检测
    • 单设备登录限制
  3. 数据加密

    • 敏感数据加密存储
    • 传输数据加密

十二、联系方式

项目负责人: [填写]
技术负责人: [填写]
项目周期: 2025-10-11 ~ 2025-10-15
文档更新日期: 2025-10-11


文档结束