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

SpringBoot + Vue 前后端分离项目实战:权限 + 工作流 + 报表
在这里插入图片描述

✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

在这里插入图片描述

📚 目录


前言

前后端分离架构已成为企业级应用开发的主流选择。本文将通过一个完整的企业管理系统实战项目,详细介绍如何使用 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 数据库设计

ER图

has

belongs

has

belongs

creates

defines

USER

bigint

id

PK

string

username

string

password

string

email

string

phone

datetime

create_time

USER_ROLE

ROLE

bigint

id

PK

string

role_name

string

role_code

string

description

ROLE_PERMISSION

PERMISSION

bigint

id

PK

string

permission_name

string

resource_type

string

resource_url

string

permission_code

WORKFLOW_INSTANCE

bigint

id

PK

string

instance_id

bigint

definition_id

bigint

starter_id

string

status

WORKFLOW_DEFINITION

bigint

id

PK

string

process_key

string

process_name

text

bpmn_xml

int

version


三、权限管理模块

3.1 RBAC权限模型

权限模型架构图

分配

拥有

关联

用户

角色

权限

资源

菜单

按钮

接口

数据

3.2 Spring Security + JWT 实现

安全配置类
@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled =true)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;@AutowiredprivateJwtRequestFilter jwtRequestFilter;@AutowiredprivateUserDetailsService jwtUserDetailsService;@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@Bean@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(HttpSecurity httpSecurity)throwsException{ httpSecurity // 禁用CSRF.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();// 添加JWT过滤器 httpSecurity.addFilterBefore(jwtRequestFilter,UsernamePasswordAuthenticationFilter.class);}}
JWT工具类
@ComponentpublicclassJwtTokenUtil{privatestaticfinalString SECRET ="enterprise-secret-key-2024";privatestaticfinallong EXPIRATION =86400000;// 24小时publicStringgenerateToken(UserDetails userDetails){Map<String,Object> claims =newHashMap<>(); claims.put("username", userDetails.getUsername());returncreateToken(claims, userDetails.getUsername());}privateStringcreateToken(Map<String,Object> claims,String subject){returnJwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+ EXPIRATION)).signWith(SignatureAlgorithm.HS512, SECRET).compact();}publicBooleanvalidateToken(String token,UserDetails userDetails){finalString username =extractUsername(token);return(username.equals(userDetails.getUsername())&&!isTokenExpired(token));}publicStringextractUsername(String token){returnextractClaims(token).getSubject();}privateClaimsextractClaims(String token){returnJwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();}privateBooleanisTokenExpired(String token){returnextractClaims(token).getExpiration().before(newDate());}}
权限注解使用
@RestController@RequestMapping("/api/user")publicclassUserController{// 需要USER角色@PreAuthorize("hasRole('USER')")@GetMapping("/profile")publicResult<UserProfile>getProfile(){returnResult.success(userService.getProfile());}// 需要USER_READ权限@PreAuthorize("hasAuthority('USER_READ')")@GetMapping("/{id}")publicResult<User>getUserById(@PathVariableLong id){returnResult.success(userService.getById(id));}// 需要ADMIN角色或当前用户ID匹配@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")@PutMapping("/{id}")publicResult<Void>updateUser(@PathVariableLong id,@RequestBodyUserDTO userDTO){ userService.updateUser(id, userDTO);returnResult.success();}}

3.3 前端权限控制

路由守卫
// router/guard.tsimport 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){// Token失效,重新登录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.tsimporttype{ 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 instanceofArray&& value.length >0){const hasPermission = permissions.some((permission:string)=>{return value.includes(permission)})if(!hasPermission){ el.parentNode?.removeChild(el)}}else{thrownewError('需要权限!如 v-permission="[\'user:add\']"')}}}exportdefault permission // main.ts 注册import permission from'./directives/permission' app.directive('permission', 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:truedatabase-type: mysql # 异步执行器async-executor-activate:trueasync-history-enabled:true# 流程定义存储process:definition-cache-limit:100# 邮件服务器配置mail:server:host: smtp.example.com port:587username: [email protected] password: password # REST API配置rest:app:enable:true

4.2 流程定义服务

@ServicepublicclassWorkflowService{@AutowiredprivateProcessEngine processEngine;@AutowiredprivateRuntimeService runtimeService;@AutowiredprivateTaskService taskService;@AutowiredprivateHistoryService historyService;/** * 部署流程定义 */publicStringdeployProcess(String processName,MultipartFile bpmnFile){Deployment deployment = processEngine.getRepositoryService().createDeployment().name(processName).addInputStream( bpmnFile.getOriginalFilename(), bpmnFile.getInputStream()).deploy();return deployment.getId();}/** * 启动流程实例 */publicStringstartProcess(String processKey,Map<String,Object> variables){ProcessInstance instance = runtimeService.startProcessInstanceByKey( processKey, variables );return instance.getId();}/** * 完成任务 */publicvoidcompleteTask(String taskId,Map<String,Object> variables){ taskService.complete(taskId, variables);}/** * 获取用户待办任务 */publicList<TaskDTO>getUserTasks(String userId){List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).orderByTaskCreateTime().desc().list();return tasks.stream().map(this::convertToDTO).collect(Collectors.toList());}/** * 获取流程历史 */publicList<HistoricActivityDTO>getProcessHistory(String processInstanceId){List<HistoricActivityInstance> activities = historyService .createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();return activities.stream().map(this::convertToDTO).collect(Collectors.toList());}/** * 获取流程图进度 */publicList<String>getActiveActivityIds(String processInstanceId){return runtimeService.getActiveActivityIds(processInstanceId);}}

4.3 请假审批流程示例

BPMN流程定义 (leave-request.bpmn20.xml)
<?xml version="1.0" encoding="UTF-8"?><definitionsxmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"xmlns:flowable="http://flowable.org/bpmn"targetNamespace="Examples"><processid="leaveRequest"name="请假审批流程"isExecutable="true"><!-- 开始节点 --><startEventid="startEvent"name="提交申请"/><!-- 员工填写请假单 --><userTaskid="fillLeaveForm"name="填写请假单"flowable:assignee="${applicant}"><extensionElements><flowable:taskListenerevent="create"class="com.enterprise.workflow.listener.LeaveTaskListener"/></extensionElements></userTask><!-- 排他网关:判断请假天数 --><exclusiveGatewayid="checkDaysGateway"name="判断天数"/><!-- 部门主管审批(3天以内) --><userTaskid="managerApproval"name="部门主管审批"flowable:candidateGroups="MANAGER"/><!-- 总监审批(3-7天) --><userTaskid="directorApproval"name="总监审批"flowable:candidateGroups="DIRECTOR"/><!-- 总经理审批(7天以上) --><userTaskid="ceoApproval"name="总经理审批"flowable:candidateGroups="CEO"/><!-- 审批结果网关 --><exclusiveGatewayid="approvalResultGateway"name="审批结果"/><!-- 审批通过 --><serviceTaskid="notifyApproved"name="发送通过通知"flowable:class="com.enterprise.workflow.delegate.ApprovedNotifyDelegate"/><!-- 审批拒绝 --><serviceTaskid="notifyRejected"name="发送拒绝通知"flowable:class="com.enterprise.workflow.delegate.RejectedNotifyDelegate"/><!-- 结束节点 --><endEventid="endEvent"name="流程结束"/><!-- 连接线 --><sequenceFlowsourceRef="startEvent"targetRef="fillLeaveForm"/><sequenceFlowsourceRef="fillLeaveForm"targetRef="checkDaysGateway"/><!-- 3天以内 --><sequenceFlowsourceRef="checkDaysGateway"targetRef="managerApproval"><conditionExpressionxsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression></sequenceFlow><!-- 3-7天 --><sequenceFlowsourceRef="checkDaysGateway"targetRef="directorApproval"><conditionExpressionxsi:type="tFormalExpression"> ${leaveDays > 3 && leaveDays <= 7} </conditionExpression></sequenceFlow><!-- 7天以上 --><sequenceFlowsourceRef="checkDaysGateway"targetRef="ceoApproval"><conditionExpressionxsi:type="tFormalExpression"> ${leaveDays > 7} </conditionExpression></sequenceFlow><sequenceFlowsourceRef="managerApproval"targetRef="approvalResultGateway"/><sequenceFlowsourceRef="directorApproval"targetRef="approvalResultGateway"/><sequenceFlowsourceRef="ceoApproval"targetRef="approvalResultGateway"/><!-- 通过 --><sequenceFlowsourceRef="approvalResultGateway"targetRef="notifyApproved"><conditionExpressionxsi:type="tFormalExpression"> ${approved == true} </conditionExpression></sequenceFlow><!-- 拒绝 --><sequenceFlowsourceRef="approvalResultGateway"targetRef="notifyRejected"><conditionExpressionxsi:type="tFormalExpression"> ${approved == false} </conditionExpression></sequenceFlow><sequenceFlowsourceRef="notifyApproved"targetRef="endEvent"/><sequenceFlowsourceRef="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 报表系统架构

报表配置

数据源配置

报表设计器

MySQL

API接口

Excel导入

拖拽设计

SQL编辑器

模板选择

报表引擎

数据查询

数据处理

格式转换

前端展示

ECharts图表

数据表格

导出功能

5.2 动态报表服务

@ServicepublicclassReportService{@AutowiredprivateJdbcTemplate jdbcTemplate;@AutowiredprivateReportConfigMapper reportConfigMapper;/** * 执行动态SQL查询 */publicList<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);}/** * 生成图表数据 */publicChartDataVOgenerateChartData(Long reportId,Map<String,Object> params){ReportConfig config = reportConfigMapper.selectById(reportId);// 查询数据List<Map<String,Object>> data =executeQuery(reportId, params);// 构建图表数据ChartDataVO chartData =newChartDataVO(); chartData.setType(config.getChartType());// 根据配置构建X轴和Y轴数据if("bar".equals(config.getChartType())||"line".equals(config.getChartType())){List<String> xAxis =newArrayList<>();List<BigDecimal> yAxis =newArrayList<>();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(newSeriesVO("数值", yAxis)));}elseif("pie".equals(config.getChartType())){List<PieDataVO> pieData =newArrayList<>();for(Map<String,Object> row : data){ pieData.add(newPieDataVO( row.get(config.getNameField()).toString(),((BigDecimal) row.get(config.getValueField())).doubleValue()));} chartData.setData(pieData);}return chartData;}/** * 导出报表 */publicvoidexportReport(Long reportId,Map<String,Object> params,HttpServletResponse response)throwsIOException{List<Map<String,Object>> data =executeQuery(reportId, params);// 使用EasyExcel导出 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);}privateStringreplaceParams(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")publicclassReportConfig{@TableId(type =IdType.AUTO)privateLong id;/** * 报表名称 */privateString reportName;/** * 报表类型: table-表格, bar-柱状图, line-折线图, pie-饼图 */privateString reportType;/** * 数据源类型: database, api */privateString dataSourceType;/** * SQL模板 */privateString sqlTemplate;/** * 图表类型 */privateString chartType;/** * X轴字段 */privateString xAxisField;/** * Y轴字段 */privateString yAxisField;/** * 名称字段(饼图用) */privateString nameField;/** * 数值字段(饼图用) */privateString valueField;/** * 参数配置(JSON格式) */privateString paramConfig;/** * 创建时间 */privateLocalDateTime 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> 

5.5 销售统计报表示例

30%25%25%20%2025年季度销售占比第一季度第二季度第三季度第四季度


六、核心代码实现

6.1 统一响应格式

@Data@NoArgsConstructor@AllArgsConstructorpublicclassResult<T>{privateInteger code;privateString message;privateT data;privateLong timestamp;publicstatic<T>Result<T>success(T data){returnnewResult<>(200,"操作成功", data,System.currentTimeMillis());}publicstatic<T>Result<T>success(){returnsuccess(null);}publicstatic<T>Result<T>error(String message){returnnewResult<>(500, message,null,System.currentTimeMillis());}publicstatic<T>Result<T>error(Integer code,String message){returnnewResult<>(code, message,null,System.currentTimeMillis());}}

6.2 全局异常处理

@RestControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{/** * 业务异常 */@ExceptionHandler(BusinessException.class)publicResult<Void>handleBusinessException(BusinessException e){ log.error("业务异常: {}", e.getMessage());returnResult.error(e.getCode(), e.getMessage());}/** * 参数校验异常 */@ExceptionHandler(MethodArgumentNotValidException.class)publicResult<Void>handleValidationException(MethodArgumentNotValidException e){String message = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(", ")); log.error("参数校验失败: {}", message);returnResult.error(400, message);}/** * 权限异常 */@ExceptionHandler(AccessDeniedException.class)publicResult<Void>handleAccessDeniedException(AccessDeniedException e){ log.error("权限不足: {}", e.getMessage());returnResult.error(403,"权限不足");}/** * 系统异常 */@ExceptionHandler(Exception.class)publicResult<Void>handleException(Exception e){ log.error("系统异常", e);returnResult.error("系统错误,请联系管理员");}}

6.3 MyBatis-Plus 配置

@Configuration@MapperScan("com.enterprise.mapper")publicclassMyBatisPlusConfig{/** * 分页插件 */@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptor interceptor =newMybatisPlusInterceptor();// 分页插件 interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));// 乐观锁插件 interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());return interceptor;}/** * 数据填充处理器 */@BeanpublicMetaObjectHandlermetaObjectHandler(){returnnewMetaObjectHandler(){@OverridepublicvoidinsertFill(MetaObject metaObject){this.strictInsertFill(metaObject,"createTime",LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());}@OverridepublicvoidupdateFill(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.0container_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:6379depends_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灵活配置、大数据量性能

学习路径

Java基础

Spring Boot

MyBatis-Plus

Spring Security

前后端分离

工作流引擎

报表系统

参考文档

Read more

华为OD技术面八股文_C++_01

华为OD技术面八股文_C++_01

文章目录 * C语言和C++的区别 * C++11引入哪些新特性 * 什么是面向对象?面向对象的三大特性 * malloc和new的区别 * delete和free的区别 * delete和delete[]的区别 * 什么是虚函数?什么是纯虚函数 * 什么是虚函数表?什么是虚函数指针? * 介绍一下虚函数实现机制 * 构造函数和构析函数能不能写为虚函数,为什么 * 说一下构造、析构函数的调用顺序 C语言和C++的区别 1. C++有新增的关键字和语法,还允许自定义命名空间。 2. C++新增类的概念,C语言中只有struct的概念。C++中添加访问权限概念,struct 的默认访问权限和继承权限都是 public,但是 class 的默认访问权限和默认继承权限都是 private. 3. C++引入了类、封装、继承、多态、模板、重载、异常处理机制等特性。而C没有 4.

By Ne0inhk
C++可变参数队列与压栈顺序:从模板语法到汇编调用约定的深度解析

C++可变参数队列与压栈顺序:从模板语法到汇编调用约定的深度解析

C++可变参数队列与压栈顺序:从模板语法到汇编调用约定的深度解析 本文聚焦一个具体而关键的技术主题:C++ 可变参数模板(Variadic Templates)。我们将从现代 C++ 的优雅写法出发,深入剖析其在 x86-64 架构下的真实行为,特别澄清一个长期被误解的核心问题——可变参数是否“从右向左压栈”?它们在寄存器和栈中究竟是如何排布的? 如果你正在实现一个类型安全的消息队列、日志系统或任务调度器,并希望理解 enqueue(1, "hello", 3.14) 这行代码在 CPU 层面到底发生了什么,那么这篇文章就是为你量身打造的。 一、引言:可变参数 ≠ va_list —— 一场范式革命 很多初学者将 C++ 的可变参数模板与 C 语言的 va_list 混为一谈。这是重大误区,甚至会导致错误的性能假设和安全漏洞。 1.1

By Ne0inhk
《C++ Stack 与 Queue 完全使用指南:基础操作 + 经典场景 + 实战习题》

《C++ Stack 与 Queue 完全使用指南:基础操作 + 经典场景 + 实战习题》

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 先搞懂基础:Stack 与 Queue 的核心特性 * 二. Stack(栈):后进先出(LIFO)的容器 * 2.1 核心特性: * 2.2 头文件与定义 * 2.3 常用接口全解析 * 2.4 基础用法演示 * 三. Queue(队列):先进先出(FIFO)的容器 * 3.1 核心特性: * 3.2 头文件与定义: * 3.

By Ne0inhk
【C++】第十七节—二叉搜索树(概念+性能分析+增删查+实现+使用场景)

【C++】第十七节—二叉搜索树(概念+性能分析+增删查+实现+使用场景)

好久不见,我是云边有个稻草人 《C++》本文所属专栏—持续更新中—欢迎订阅 目录 一、二叉搜索树的概念 二、二叉搜索树的性能分析 三、二叉搜索树的插入 SearchBinaryTree.h test.cpp 四、⼆叉搜索树的查找 【只有一个3】 【有多个3】  五、⼆叉搜索树的删除 六、二叉搜索树的实现代码 SearchBinaryTree.h test.cpp  七、二叉搜索树key和key/value使用场景 7.1 key搜索场景 7.2 key/value搜索场景 7.3 key/value⼆叉搜索树代码实现 .h .cpp 正文开始—— 一、二叉搜索树的概念 ⼆叉搜索树⼜

By Ne0inhk