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

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

基于 SpringBoot 和 Vue 3 构建的企业级管理系统,涵盖 RBAC 权限模型、Flowable 工作流引擎集成及动态报表功能。后端采用 Spring Security + JWT 实现细粒度控制,前端通过路由守卫与自定义指令完成权限拦截。系统支持可视化流程设计、条件分支审批及 ECharts 数据展示,并通过 Docker Compose 实现容器化部署。

赛博朋克发布于 2026/3/23更新于 2026/7/2635 浏览
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/ # 项目文档

二、系统架构设计

2.1 整体架构图

  • 前端层:Vue 3 应用、Element Plus UI、Vue Router、Pinia Store
  • 网关层:Nginx 反向代理、API Gateway、认证服务
  • 应用层:用户服务、权限服务、工作流服务、报表服务
  • 数据层:MySQL 数据库、Redis 缓存、Flowable DB

2.2 数据库设计

主要实体包括 USER(用户)、ROLE(角色)、PERMISSION(权限)、WORKFLOW_INSTANCE(流程实例)等。采用 RBAC 模型,用户关联角色,角色关联权限。

三、权限管理模块

3.1 RBAC 权限模型

权限模型遵循 用户 - 角色 - 权限 的层级关系,支持菜单、按钮及接口的细粒度控制。

3.2 Spring Security + JWT 实现

安全配置类
@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 工具类
@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());
    }
}
权限注解使用
@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 前端权限控制

路由守卫
// 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}`)
        }
    }
})
自定义权限指令
// 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 集成配置

Maven 依赖
<dependencies>
    <!-- Flowable 核心依赖 -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-spring-boot-starter</artifactId>
        <version>7.0.0</version>
    </dependency>
    <!-- Flowable REST API -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-rest</artifactId>
        <version>7.0.0</version>
    </dependency>
</dependencies>
配置文件
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 流程定义服务

@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 流程定义 (leave-request.bpmn20.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 前端流程图组件

<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 编辑器、模板选择,通过 API 接口返回数据,前端使用 ECharts 进行图表展示,并支持 Excel 导出。

5.2 动态报表服务

@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 前端报表组件

<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 统一响应格式

@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
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 配置

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 BPMN流程设计、复杂网关、任务监听
报表系统ECharts + 动态 SQL灵活配置、大数据量性能

参考文档

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

目录

  1. SpringBoot + Vue 前后端分离:权限、工作流与报表实现
  2. 一、项目背景与技术选型
  3. 1.1 技术栈总览
  4. 1.2 项目结构
  5. 二、系统架构设计
  6. 2.1 整体架构图
  7. 2.2 数据库设计
  8. 三、权限管理模块
  9. 3.1 RBAC 权限模型
  10. 3.2 Spring Security + JWT 实现
  11. 安全配置类
  12. JWT 工具类
  13. 权限注解使用
  14. 3.3 前端权限控制
  15. 路由守卫
  16. 自定义权限指令
  17. 四、工作流引擎集成
  18. 4.1 Flowable 集成配置
  19. Maven 依赖
  20. 配置文件
  21. 4.2 流程定义服务
  22. 4.3 请假审批流程示例
  23. BPMN 流程定义 (leave-request.bpmn20.xml)
  24. 4.4 前端流程图组件
  25. 五、报表系统实现
  26. 5.1 报表系统架构
  27. 5.2 动态报表服务
  28. 5.3 报表配置实体
  29. 5.4 前端报表组件
  30. 六、核心代码实现
  31. 6.1 统一响应格式
  32. 6.2 全局异常处理
  33. 6.3 MyBatis-Plus 配置
  34. 七、部署与运维
  35. 7.1 Docker 容器化部署
  36. 后端 Dockerfile
  37. Docker Compose 编排
  38. 7.2 CI/CD 流程
  39. 7.3 Nginx 配置
  40. 八、总结
  41. 项目技术要点
  42. 参考文档
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 宇树机器人 SDK2 开发指南:环境搭建与 Demo 测试
  • 机器人动力学:牛顿欧拉法推导与详解
  • NSSCTF Web 安全竞赛解题思路汇总
  • OpenMAIC:清华开源 AI 教学平台,支持文档生成课程
  • Jenkins+docker容器部署前端Vue项目详细教程
  • video-analyzer:基于 AI 的视频内容分析与摘要工具
  • C++ 基础入门:输出语句 cout 用法详解
  • 阿里开源 Page-Agent:一行 JS 代码实现大模型前端 DOM 控制
  • 设计支持万人并发抢购的秒杀系统架构方案
  • 具身智能:机器人训练核心流程与关键技术
  • LeetCode 86: 分隔链表
  • Linux 下 OpenClaw 快速安装、初始化及 Web UI 配置指南
  • AI 时代技术民主化:文科生为何成最大受益者
  • Gitee 与 Git 入门指南:从零开始掌握版本控制
  • C++ List 容器实现原理与代码详解(下)
  • Arduino BLDC 机器人 IMU 角度读取与 PID 互补滤波控制
  • 腾讯云轻量服务器部署 OpenClaw 并接入飞书指南
  • 开源大模型深度评测:四大免费模型场景化对比与选型指南
  • Alpine Linux + JDK 兼容性指南:jstack 在容器中为何不可用及解法
  • GitHub Copilot 登录失败常见原因与排查指南

相关免费在线工具

  • 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