跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java

SpringBoot + Vue 前后端分离实战:权限、工作流与报表

基于 SpringBoot 与 Vue 构建的企业级管理系统,涵盖 RBAC 权限模型、Flowable 工作流引擎及动态报表功能。后端采用 Spring Security + JWT 实现细粒度控制,前端通过路由守卫与自定义指令管理权限。集成 Flowable 支持可视化流程设计与审批,结合 ECharts 与 MyBatis-Plus 实现灵活的数据可视化与查询。提供 Docker 容器化部署方案,适合中大型业务场景。

dehua dong发布于 2026/3/24更新于 2026/9/11102 浏览
SpringBoot + Vue 前后端分离实战:权限、工作流与报表

SpringBoot + Vue 前后端分离实战:权限、工作流与报表

前后端分离架构已成为企业级应用开发的主流选择。本文将通过一个完整的企业管理系统实战项目,详细介绍如何使用 SpringBoot + Vue 技术栈,实现权限管理、工作流引擎和报表系统三大核心功能。

一、项目背景与技术选型

1.1 技术栈总览

层次技术选型说明
前端框架Vue 3 + TypeScript渐进式框架
UI 组件库Element Plus企业级组件库
状态管理PiniaVue 3 官方推荐
后端框架Spring Boot 2.7Java 微服务框架
ORM 框架MyBatis-Plus持久层增强
数据库MySQL 8.0关系型数据库
缓存Redis 6.0高性能缓存
工作流引擎Flowable 7.0开源 BPMN 平台
报表工具ECharts + UReport2数据可视化

1.2 项目结构

enterprise-system/
├── enterprise-admin/ # 前端项目
│   ├── src/
│   │   ├── api/ # API 接口
│   │   ├── assets/ # 静态资源
│   │   ├── components/ # 公共组件
│   │   ├── views/ # 页面视图
│   │   ├── router/ # 路由配置
│   │   ├── store/ # 状态管理
│   │   └── utils/ # 工具函数
│   └── package.json
├── enterprise-server/ # 后端项目
│   ├── src/main/java/com/enterprise/
│   │   ├── controller/ # 控制层
│   │   ├── service/ # 业务层
│   │   ├── mapper/ # 数据访问层
│   │   ├── entity/ # 实体类
│   │   ├── dto/ # 数据传输对象
│   │   ├── config/ # 配置类
│   │   └── security/ # 安全相关
│   └── pom.xml
└── docs/ # 项目文档

二、系统架构设计

整体架构采用分层设计,从前端到数据层依次递进。前端通过 Nginx 反向代理接入,后端由网关统一处理请求,再分发至认证、用户、权限、工作流及报表等微服务模块。数据存储方面,MySQL 负责持久化,Redis 承担缓存任务,Flowable 拥有独立的流程数据库。

2.1 数据库设计

核心表包括用户表(USER)、角色表(ROLE)、权限表(PERMISSION)以及工作流相关的实例定义表。用户与角色多对多关联,角色与权限多对多关联,形成经典的 RBAC 模型基础。

三、权限管理模块

3.1 RBAC 权限模型

权限体系围绕用户、角色、权限和资源展开。菜单、按钮和接口均作为资源节点进行管控,确保细粒度的访问控制。

3.2 Spring Security + JWT 实现

安全配置类

这里我们使用 WebSecurityConfigurerAdapter 来配置安全策略。注意在实际生产中,建议将敏感信息如密钥放入环境变量中。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    @Autowired
    private UserDetailsService jwtUserDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .csrf().disable()
            .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests()
            .antMatchers("/api/auth/**", "/api/public/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
JWT 工具类

生成 Token 时设置过期时间,验证时需检查签名和有效期。实际项目中请确保 SECRET 足够复杂且保密。

@Component
public class JwtTokenUtil {
    private static final String SECRET = "enterprise-secret-key-2024"; // 生产环境请使用环境变量
    private static final long EXPIRATION = 86400000; // 24 小时

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", userDetails.getUsername());
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    public String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }

    private Claims extractClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractClaims(token).getExpiration().before(new Date());
    }
}
权限注解使用

在 Controller 层面直接标注权限要求,配合 Spring Security 的表达式语言,可以灵活控制接口访问。

@RestController
@RequestMapping("/api/user")
public class UserController {
    // 需要 USER 角色
    @PreAuthorize("hasRole('USER')")
    @GetMapping("/profile")
    public Result<UserProfile> getProfile() {
        return Result.success(userService.getProfile());
    }

    // 需要 USER_READ 权限
    @PreAuthorize("hasAuthority('USER_READ')")
    @GetMapping("/{id}")
    public Result<User> getUserById(@PathVariable Long id) {
        return Result.success(userService.getById(id));
    }

    // 需要 ADMIN 角色或当前用户 ID 匹配
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    @PutMapping("/{id}")
    public Result<Void> updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO) {
        userService.updateUser(id, userDTO);
        return Result.success();
    }
}

3.3 前端权限控制

路由守卫

登录态校验是前端的第一道防线。获取 Token 后,动态加载用户拥有的路由,防止未授权访问。

// router/guard.ts
import router from './index'
import { useUserStore } from '@/store/user'
import { getToken } from '@/utils/auth'
import { ElMessage } from 'element-plus'

const whiteList = ['/login', '/404', '/403']

router.beforeEach(async (to, from, next) => {
    const userStore = useUserStore()
    const hasToken = getToken()

    if (hasToken) {
        if (to.path === '/login') {
            next({ path: '/' })
        } else {
            if (userStore.userId) {
                next()
            } else {
                try {
                    await userStore.getUserInfo()
                    await userStore.generateRoutes()
                    next({ ...to, replace: true })
                } catch (error) {
                    await userStore.logout()
                    ElMessage.error('登录状态已过期,请重新登录')
                    next(`/login?redirect=${to.path}`)
                }
            }
        }
    } else {
        if (whiteList.includes(to.path)) {
            next()
        } else {
            next(`/login?redirect=${to.path}`)
        }
    }
})
自定义权限指令

对于按钮级别的权限控制,Vue 自定义指令非常实用。它能在 DOM 挂载时自动判断并移除无权限元素。

// directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue'
import { useUserStore } from '@/store/user'

const permission: Directive = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        const { value } = binding
        const userStore = useUserStore()
        const permissions = userStore.permissions

        if (value && value instanceof Array && value.length > 0) {
            const hasPermission = permissions.some((permission: string) => {
                return value.includes(permission)
            })
            if (!hasPermission) {
                el.parentNode?.removeChild(el)
            }
        } else {
            throw new Error('需要权限!如 v-permission="[\'user:add\']"')
        }
    }
}

export default permission

模板中使用非常简单:

<template>
  <div>
    <!-- 有删除权限时显示删除按钮 -->
    <el-button v-permission="['user:delete']" type="danger" @click="handleDelete"> 删除 </el-button>
    <!-- 多个权限满足其一即可 -->
    <el-button v-permission="['user:edit', 'user:audit']" type="primary" @click="handleEdit"> 编辑 </el-button>
  </div>
</template>

四、工作流引擎集成

4.1 Flowable 集成配置

引入 Flowable 依赖后,需在配置文件中指定数据库 Schema 更新策略,并开启异步执行器以提升性能。

flowable:
  database-schema-update: true
  database-type: mysql
  async-executor-activate: true
  async-history-enabled: true
  process:
    definition-cache-limit: 100
  mail:
    server:
      host: smtp.example.com
      port: 587
      username: [email protected]
      password: password
  rest:
    app:
      enable: true

4.2 流程定义服务

后端封装了常用的流程操作,如部署、启动、完成任务及查询历史。这些方法屏蔽了底层 API 的复杂性。

@Service
public class WorkflowService {
    @Autowired
    private ProcessEngine processEngine;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private HistoryService historyService;

    /**
     * 部署流程定义
     */
    public String deployProcess(String processName, MultipartFile bpmnFile) {
        Deployment deployment = processEngine.getRepositoryService()
                .createDeployment()
                .name(processName)
                .addInputStream(bpmnFile.getOriginalFilename(), bpmnFile.getInputStream())
                .deploy();
        return deployment.getId();
    }

    /**
     * 启动流程实例
     */
    public String startProcess(String processKey, Map<String, Object> variables) {
        ProcessInstance instance = runtimeService.startProcessInstanceByKey(processKey, variables);
        return instance.getId();
    }

    /**
     * 完成任务
     */
    public void completeTask(String taskId, Map<String, Object> variables) {
        taskService.complete(taskId, variables);
    }

    /**
     * 获取用户待办任务
     */
    public List<TaskDTO> getUserTasks(String userId) {
        List<Task> tasks = taskService.createTaskQuery()
                .taskAssignee(userId)
                .orderByTaskCreateTime()
                .desc()
                .list();
        return tasks.stream().map(this::convertToDTO).collect(Collectors.toList());
    }

    /**
     * 获取流程历史
     */
    public List<HistoricActivityDTO> getProcessHistory(String processInstanceId) {
        List<HistoricActivityInstance> activities = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .orderByHistoricActivityInstanceStartTime()
                .asc()
                .list();
        return activities.stream().map(this::convertToDTO).collect(Collectors.toList());
    }

    /**
     * 获取流程图进度
     */
    public List<String> getActiveActivityIds(String processInstanceId) {
        return runtimeService.getActiveActivityIds(processInstanceId);
    }
}

4.3 请假审批流程示例

这是一个典型的条件分支流程。根据请假天数不同,流转至不同的审批人(主管、总监、总经理)。BPMN XML 定义了节点间的流转逻辑。

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:flowable="http://flowable.org/bpmn"
             targetNamespace="Examples">
    <process id="leaveRequest" name="请假审批流程" isExecutable="true">
        <startEvent id="startEvent" name="提交申请"/>
        <userTask id="fillLeaveForm" name="填写请假单" flowable:assignee="${applicant}">
            <extensionElements>
                <flowable:taskListener event="create" class="com.enterprise.workflow.listener.LeaveTaskListener"/>
            </extensionElements>
        </userTask>
        <exclusiveGateway id="checkDaysGateway" name="判断天数"/>
        <userTask id="managerApproval" name="部门主管审批" flowable:candidateGroups="MANAGER"/>
        <userTask id="directorApproval" name="总监审批" flowable:candidateGroups="DIRECTOR"/>
        <userTask id="ceoApproval" name="总经理审批" flowable:candidateGroups="CEO"/>
        <exclusiveGateway id="approvalResultGateway" name="审批结果"/>
        <serviceTask id="notifyApproved" name="发送通过通知" flowable:class="com.enterprise.workflow.delegate.ApprovedNotifyDelegate"/>
        <serviceTask id="notifyRejected" name="发送拒绝通知" flowable:class="com.enterprise.workflow.delegate.RejectedNotifyDelegate"/>
        <endEvent id="endEvent" name="流程结束"/>
        <sequenceFlow sourceRef="startEvent" targetRef="fillLeaveForm"/>
        <sequenceFlow sourceRef="fillLeaveForm" targetRef="checkDaysGateway"/>
        <sequenceFlow sourceRef="checkDaysGateway" targetRef="managerApproval">
            <conditionExpression xsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="checkDaysGateway" targetRef="directorApproval">
            <conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 3 && leaveDays <= 7} </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="checkDaysGateway" targetRef="ceoApproval">
            <conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 7} </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="managerApproval" targetRef="approvalResultGateway"/>
        <sequenceFlow sourceRef="directorApproval" targetRef="approvalResultGateway"/>
        <sequenceFlow sourceRef="ceoApproval" targetRef="approvalResultGateway"/>
        <sequenceFlow sourceRef="approvalResultGateway" targetRef="notifyApproved">
            <conditionExpression xsi:type="tFormalExpression"> ${approved == true} </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="approvalResultGateway" targetRef="notifyRejected">
            <conditionExpression xsi:type="tFormalExpression"> ${approved == false} </conditionExpression>
        </sequenceFlow>
        <sequenceFlow sourceRef="notifyApproved" targetRef="endEvent"/>
        <sequenceFlow sourceRef="notifyRejected" targetRef="endEvent"/>
    </process>
</definitions>

4.4 前端流程图组件

利用 bpmn-js 渲染 XML 定义的流程图,并高亮当前活动节点,让用户直观看到流程进度。

<template>
  <div>
    <div ref="bpmnCanvas"></div>
    <div>
      <el-tag>当前环节:{{ currentTaskName }}</el-tag>
      <el-tag type="success">处理人:{{ currentAssignee }}</el-tag>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import BpmnJS from 'bpmn-js/lib/NavigatedViewer'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'

interface Props {
  processInstanceId: string
  xml: string
  activeActivityIds: string[]
}

const props = defineProps<Props>()
const bpmnCanvas = ref<HTMLElement>()
const currentTaskName = ref('')
const currentAssignee = ref('')
let bpmnViewer: BpmnJS | null = null

onMounted(async () => {
  if (bpmnCanvas.value) {
    bpmnViewer = new BpmnJS({ container: bpmnCanvas.value })
    try {
      await bpmnViewer.importXML(props.xml)
      const canvas = bpmnViewer.get('canvas')
      const elementRegistry = bpmnViewer.get('elementRegistry')
      props.activeActivityIds.forEach(id => {
        const element = elementRegistry.get(id)
        if (element) {
          canvas.addMarker(id, 'highlight')
        }
      })
    } catch (error) {
      console.error('流程图渲染失败:', error)
    }
  }
})
</script>

<style>
.bpmn-canvas {
  height: 600px;
  border: 1px solid #ddd;
}
.highlight {
  fill: #67C23A !important;
  stroke: #67C23A !important;
}
</style>

五、报表系统实现

5.1 报表系统架构

报表系统支持拖拽设计和 SQL 编辑器,后端提供动态 SQL 执行能力,前端结合 ECharts 展示图表或表格,并支持 Excel 导出。

5.2 动态报表服务

通过配置化的 SQL 模板和参数替换,实现灵活的查询。同时根据配置类型构建对应的图表数据结构。

@Service
public class ReportService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private ReportConfigMapper reportConfigMapper;

    /**
     * 执行动态 SQL 查询
     */
    public List<Map<String, Object>> executeQuery(Long reportId, Map<String, Object> params) {
        ReportConfig config = reportConfigMapper.selectById(reportId);
        String sql = replaceParams(config.getSqlTemplate(), params);
        return jdbcTemplate.queryForList(sql);
    }

    /**
     * 生成图表数据
     */
    public ChartDataVO generateChartData(Long reportId, Map<String, Object> params) {
        ReportConfig config = reportConfigMapper.selectById(reportId);
        List<Map<String, Object>> data = executeQuery(reportId, params);
        ChartDataVO chartData = new ChartDataVO();
        chartData.setType(config.getChartType());

        if ("bar".equals(config.getChartType()) || "line".equals(config.getChartType())) {
            List<String> xAxis = new ArrayList<>();
            List<BigDecimal> yAxis = new ArrayList<>();
            for (Map<String, Object> row : data) {
                xAxis.add(row.get(config.getXAxisField()).toString());
                yAxis.add((BigDecimal) row.get(config.getYAxisField()));
            }
            chartData.setXAxis(xAxis);
            chartData.setSeries(Collections.singletonList(new SeriesVO("数值", yAxis)));
        } else if ("pie".equals(config.getChartType())) {
            List<PieDataVO> pieData = new ArrayList<>();
            for (Map<String, Object> row : data) {
                pieData.add(new PieDataVO(
                        row.get(config.getNameField()).toString(),
                        ((BigDecimal) row.get(config.getValueField())).doubleValue()));
            }
            chartData.setData(pieData);
        }
        return chartData;
    }

    /**
     * 导出报表
     */
    public void exportReport(Long reportId, Map<String, Object> params, HttpServletResponse response) throws IOException {
        List<Map<String, Object>> data = executeQuery(reportId, params);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-disposition", "attachment;filename=report.xlsx");
        EasyExcel.write(response.getOutputStream()).sheet("报表数据").doWrite(data);
    }

    private String replaceParams(String template, Map<String, Object> params) {
        String result = template;
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            result = result.replace("${" + entry.getKey() + "}", String.valueOf(entry.getValue()));
        }
        return result;
    }
}

5.3 报表配置实体

@Data
@TableName("sys_report_config")
public class ReportConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String reportName;
    private String reportType; // table-表格,bar-柱状图,line-折线图,pie-饼图
    private String dataSourceType; // database, api
    private String sqlTemplate;
    private String chartType;
    private String xAxisField;
    private String yAxisField;
    private String nameField;
    private String valueField;
    private String paramConfig;
    private LocalDateTime createTime;
}

5.4 前端报表组件

前端负责接收配置,渲染查询表单,并根据返回数据调用 ECharts 或 Element Table 进行展示。

<template>
  <div>
    <el-form :model="queryParams" inline>
      <el-form-item v-for="param in paramList" :key="param.name" :label="param.label">
        <el-input v-if="param.type === 'input'" v-model="queryParams[param.name]" />
        <el-date-picker v-else-if="param.type === 'date'" v-model="queryParams[param.name]" type="daterange" />
        <el-select v-else-if="param.type === 'select'" v-model="queryParams[param.name]">
          <el-option v-for="opt in param.options" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="loadReport">查询</el-button>
        <el-button @click="exportReport">导出</el-button>
      </el-form-item>
    </el-form>
    <div v-if="reportConfig.reportType === 'chart'" ref="chartRef"></div>
    <el-table v-else :data="tableData" border>
      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" />
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { getReportData, exportReportData } from '@/api/report'

interface Props {
  reportId: number
}

const props = defineProps<Props>()
const chartRef = ref<HTMLElement>()
const chartInstance = ref<echarts.ECharts>()
const queryParams = ref<Record<string, any>>({})
const paramList = ref<any[]>([])
const reportConfig = ref<any>({})
const tableData = ref<any[]>([])
const columns = ref<any[]>([])

onMounted(async () => {
  const config = await getReportConfig(props.reportId)
  reportConfig.value = config
  paramList.value = JSON.parse(config.paramConfig || '[]')

  if (config.reportType === 'chart' && chartRef.value) {
    chartInstance.value = echarts.init(chartRef.value)
  }
  await loadReport()
})

onUnmounted(() => {
  chartInstance.value?.dispose()
})

async function loadReport() {
  const data = await getReportData(props.reportId, queryParams.value)
  if (reportConfig.value.reportType === 'chart') {
    renderChart(data)
  } else {
    tableData.value = data.list
    columns.value = data.columns
  }
}

function renderChart(data: any) {
  const option = generateChartOption(reportConfig.value.chartType, data)
  chartInstance.value?.setOption(option)
}

function generateChartOption(type: string, data: any) {
  const baseOption = {
    tooltip: { trigger: 'axis' },
    legend: { data: data.series?.map((s: any) => s.name) },
  }
  if (type === 'bar' || type === 'line') {
    return {
      ...baseOption,
      xAxis: { type: 'category', data: data.xAxis },
      yAxis: { type: 'value' },
      series: data.series?.map((s: any) => ({ name: s.name, type: type, data: s.data }))
    }
  } else if (type === 'pie') {
    return {
      tooltip: { trigger: 'item' },
      series: [{ type: 'pie', data: data.data }]
    }
  }
  return baseOption
}

async function exportReport() {
  await exportReportData(props.reportId, queryParams.value)
}
</script>

<style scoped>
.chart {
  height: 500px;
  margin-top: 20px;
}
</style>

六、核心代码实现

6.1 统一响应格式

所有接口返回统一的 Result 对象,包含状态码、消息、数据和时间戳,便于前端统一处理。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    private Long timestamp;

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data, System.currentTimeMillis());
    }

    public static <T> Result<T> success() {
        return success(null);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null, System.currentTimeMillis());
    }

    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null, System.currentTimeMillis());
    }
}

6.2 全局异常处理

捕获业务异常、参数校验异常及系统异常,避免将堆栈信息直接暴露给前端,提升安全性。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        log.error("参数校验失败:{}", message);
        return Result.error(400, message);
    }

    @ExceptionHandler(AccessDeniedException.class)
    public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
        log.error("权限不足:{}", e.getMessage());
        return Result.error(403, "权限不足");
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统错误,请联系管理员");
    }
}

6.3 MyBatis-Plus 配置

启用分页插件和乐观锁插件,并配置自动填充创建时间和更新时间。

@Configuration
@MapperScan("com.enterprise.mapper")
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MetaObjectHandler() {
            @Override
            public void insertFill(MetaObject metaObject) {
                this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
                this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
            }

            @Override
            public void updateFill(MetaObject metaObject) {
                this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
            }
        };
    }
}

七、部署与运维

7.1 Docker 容器化部署

使用 Dockerfile 构建后端镜像,并通过 Docker Compose 编排 MySQL、Redis、后端服务和前端 Nginx,简化环境依赖。

后端 Dockerfile
FROM openjdk:11-jre-slim
LABEL maintainer="enterprise-system"
WORKDIR /app
COPY target/enterprise-server.jar app.jar
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Xms512m", "-Xmx1024m", "app.jar"]
Docker Compose 编排
version: '3.8'
services:
  mysql:
    image: mysql:8.0
    container_name: enterprise-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123456
      MYSQL_DATABASE: enterprise_db
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./sql:/docker-entrypoint-initdb.d
    networks:
      - enterprise-network

  redis:
    image: redis:6.2-alpine
    container_name: enterprise-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - enterprise-network

  backend:
    build: ./enterprise-server
    container_name: enterprise-backend
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/enterprise_db?useSSL=false
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: root123456
      SPRING_REDIS_HOST: redis
      SPRING_REDIS_PORT: 6379
    depends_on:
      - mysql
      - redis
    networks:
      - enterprise-network

  frontend:
    image: nginx:alpine
    container_name: enterprise-frontend
    ports:
      - "80:80"
    volumes:
      - ./enterprise-admin/dist:/usr/share/nginx/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - backend
    networks:
      - enterprise-network

volumes:
  mysql-data:
  redis-data:

networks:
  enterprise-network:
    driver: bridge

7.2 CI/CD 流程

开发提交代码后触发 Gitlab,经过 Maven 构建、单元测试、Docker 构建、推送镜像,最终部署到测试或生产环境,并进行健康检查。

7.3 Nginx 配置

配置反向代理转发 API 请求,并处理前端静态资源的 SPA 路由问题。

upstream backend {
  server backend:8080;
}

server {
  listen 80;
  server_name your-domain.com;

  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html;
  }

  location /api/ {
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  client_max_body_size 100M;

  gzip on;
  gzip_types text/plain text/css application/json application/javascript;
}

八、总结

本项目涵盖了企业级应用的核心要素。权限管理采用 Spring Security + JWT 实现细粒度控制;工作流集成 Flowable 支持复杂审批场景;报表系统结合 ECharts 提供灵活的数据可视化。通过 Docker 容器化部署,确保了环境的一致性和易维护性。这套方案适合中大型业务系统的快速搭建与迭代。

参考文档:

  • Spring Boot 官方文档
  • Vue 3 官方文档
  • Flowable 用户指南
  • Element Plus 组件库

目录

  1. SpringBoot + Vue 前后端分离实战:权限、工作流与报表
  2. 一、项目背景与技术选型
  3. 1.1 技术栈总览
  4. 1.2 项目结构
  5. 二、系统架构设计
  6. 2.1 数据库设计
  7. 三、权限管理模块
  8. 3.1 RBAC 权限模型
  9. 3.2 Spring Security + JWT 实现
  10. 安全配置类
  11. JWT 工具类
  12. 权限注解使用
  13. 3.3 前端权限控制
  14. 路由守卫
  15. 自定义权限指令
  16. 四、工作流引擎集成
  17. 4.1 Flowable 集成配置
  18. 4.2 流程定义服务
  19. 4.3 请假审批流程示例
  20. 4.4 前端流程图组件
  21. 五、报表系统实现
  22. 5.1 报表系统架构
  23. 5.2 动态报表服务
  24. 5.3 报表配置实体
  25. 5.4 前端报表组件
  26. 六、核心代码实现
  27. 6.1 统一响应格式
  28. 6.2 全局异常处理
  29. 6.3 MyBatis-Plus 配置
  30. 七、部署与运维
  31. 7.1 Docker 容器化部署
  32. 后端 Dockerfile
  33. Docker Compose 编排
  34. 7.2 CI/CD 流程
  35. 7.3 Nginx 配置
  36. 八、总结

更多推荐文章

查看全部
  • VS Code 关闭 Copilot 代码 AI 补全
  • WSL 环境下 Git 安装与配置指南
  • Node.js 下载安装与环境配置全流程
  • CentOS 7 源码编译安装 Nginx 完整流程
  • vkedit:基于 Vue3 的 Web 图形编辑器库,支持标签与二维码设计
  • SQL Server 2008 R2 安装教程及常见错误解决
  • Dev C++ 安装及使用方法教程
  • 智能家居 AI 应用变革:从指令执行到主动理解
  • OpenClaw 部署飞书机器人
  • Claude Code 安装配置与使用指南(Windows/macOS)
  • 常用 C/C++ 编译器盘点:Clang、MSVC、ICC 及嵌入式方案
  • LLaMA-Factory 命令行工具常用功能与实操
  • C++ 特殊类设计:拷贝控制、内存分配与单例模式
  • 17:无人机远程执行路径规划:A*算法与GPS精准打击
  • Java 常用 API 与日期工具详解及异常处理
  • Python OCR 文字识别:pytesseract 安装与配置指南
  • GitHub Copilot 定制指南:Awesome GitHub Copilot 项目解析
  • OpenClaw:AI 行动时代的破局者与核心架构解析
  • Ubuntu 24.04 安装 NVIDIA 显卡驱动及 CUDA 开发环境指南
  • FPGA 组成原理:IO 资源详解

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online