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

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

基于 SpringBoot 和 Vue 3 构建的企业级管理系统,涵盖 RBAC 权限控制、Flowable 工作流引擎集成及动态报表生成。核心实现包括 JWT 认证、动态路由守卫、BPMN 流程定义可视化以及 ECharts 数据展示。项目采用前后端分离架构,支持 Docker 容器化部署与 CI/CD 自动化流程,适用于复杂业务场景下的快速开发与运维。

编程诗人发布于 2026/3/24更新于 2026/6/1115 浏览
SpringBoot + Vue 前后端分离项目:权限、工作流与报表实现

SpringBoot + Vue 前后端分离项目实战

前言

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

项目特色

  • 前后端分离:RESTful API 设计,便于扩展和维护
  • RBAC 权限模型:细粒度的权限控制体系
  • Flowable 工作流:可视化流程设计与执行
  • 动态报表:灵活配置的数据可视化方案

一、项目背景与技术选型

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 API 配置
  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="判断天数"/>
        <!-- 部门主管审批(3 天以内) -->
        <userTask id="managerApproval" name="部门主管审批" flowable:candidateGroups="MANAGER"/>
        <!-- 总监审批(3-7 天) -->
        <userTask id="directorApproval" name="总监审批" flowable:candidateGroups="DIRECTOR"/>
        <!-- 总经理审批(7 天以上) -->
        <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"/>
        <!-- 3 天以内 -->
        <sequenceFlow sourceRef="checkDaysGateway" targetRef="managerApproval">
            <conditionExpression xsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression>
        </sequenceFlow>
        <!-- 3-7 天 -->
        <sequenceFlow sourceRef="checkDaysGateway" targetRef="directorApproval">
            <conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 3 && leaveDays <= 7} </conditionExpression>
        </sequenceFlow>
        <!-- 7 天以上 -->
        <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 接口连接 MySQL 数据源。前端展示 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;

    /**
     * 报表类型:table-表格,bar-柱状图,line-折线图,pie-饼图
     */
    private String reportType;

    /**
     * 数据源类型:database, api
     */
    private String dataSourceType;

    /**
     * SQL 模板
     */
    private String sqlTemplate;

    /**
     * 图表类型
     */
    private String chartType;

    /**
     * X 轴字段
     */
    private String xAxisField;

    /**
     * Y 轴字段
     */
    private String yAxisField;

    /**
     * 名称字段 (饼图用)
     */
    private String nameField;

    /**
     * 数值字段 (饼图用)
     */
    private String valueField;

    /**
     * 参数配置 (JSON 格式)
     */
    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
# 添加 JAR 文件
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 数据库
  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 缓存
  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;
    }

    # API 代理
    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 压缩
    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. 项目特色
  4. 一、项目背景与技术选型
  5. 1.1 技术栈总览
  6. 1.2 项目结构
  7. 二、系统架构设计
  8. 2.1 整体架构
  9. 2.2 数据库设计
  10. 三、权限管理模块
  11. 3.1 RBAC 权限模型
  12. 3.2 Spring Security + JWT 实现
  13. 安全配置类
  14. JWT 工具类
  15. 权限注解使用
  16. 3.3 前端权限控制
  17. 路由守卫
  18. 自定义权限指令
  19. 组件中使用
  20. 四、工作流引擎集成
  21. 4.1 Flowable 集成配置
  22. Maven 依赖
  23. 配置文件
  24. 数据库配置
  25. 异步执行器
  26. 流程定义存储
  27. 邮件服务器配置
  28. REST API 配置
  29. 4.2 流程定义服务
  30. 4.3 请假审批流程示例
  31. BPMN 流程定义 (leave-request.bpmn20.xml)
  32. 4.4 前端流程图组件
  33. 五、报表系统实现
  34. 5.1 报表系统架构
  35. 5.2 动态报表服务
  36. 5.3 报表配置实体
  37. 5.4 前端报表组件
  38. 六、核心代码实现
  39. 6.1 统一响应格式
  40. 6.2 全局异常处理
  41. 6.3 MyBatis-Plus 配置
  42. 七、部署与运维
  43. 7.1 Docker 容器化部署
  44. 后端 Dockerfile
  45. 添加 JAR 文件
  46. 设置时区
  47. 暴露端口
  48. 启动应用
  49. Docker Compose 编排
  50. MySQL 数据库
  51. Redis 缓存
  52. 后端应用
  53. 前端应用
  54. 7.2 CI/CD 流程
  55. 7.3 Nginx 配置
  56. 八、总结
  57. 项目技术要点
  58. 参考文档
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • OpenClaw 部署指南:模型接入与飞书机器人配置
  • MyBatisPlus 与 Thymeleaf 全栈分页实战方案
  • 基于 Spring Boot 与 WebSocket 的 Java 实时聊天室系统
  • 闲置小米 9 变身复古掌机:天马 G 前端实战
  • 基于 Java 和 Leaflet 的湖南省道路长度 WebGIS 系统构建
  • GitHub 开源游戏项目与引擎资源汇总
  • Python 绘制条形图和直方图实战教程
  • DeepSeek 各版本说明与优缺点分析
  • OpenClaw 实战部署:从环境搭建到自动化工作流配置指南
  • Trae IDE 安装与使用教程
  • 基于 B/S 架构的 Web 化 PACS/RIS 系统设计解析
  • Python 异步 IO 入门:深入理解 Asyncio 核心机制
  • AI 辅助编程:如何利用 GitHub Copilot 等工具提升开发效率
  • Dify 工作流发布为 MCP Server 实战指南
  • 后仿真 SDF 反标常见 Warning 解析与处理方案
  • HarmonyOS 网络请求实战:Axios 集成与用户列表交互
  • 基于 SpringBoot 的青年公寓服务平台
  • Linux 命名管道(FIFO)通信:原理与跨进程实战
  • 应届生春招避坑指南:识别三无公司与保障劳动权益
  • 算法的“预谋”:前缀和如何改变游戏规则

相关免费在线工具

  • 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