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

【Spring】原理解析:Spring Boot 自动配置

【Spring】原理解析:Spring Boot 自动配置

目录 1.前言 插播一条消息~ 2.正文 2.1加载bean到容器中 2.1.1 @ComponentScan:主动扫描发现Bean 2.1.2 @Import:灵活导入Bean的“万能钥匙” 2.1.3 自定义注解:封装配置的“快捷方式” 2.2Spring Boot原理分析 2.2.1 @SpringBootApplication组合注解 2.2.2 @EnableAutoConfiguration:自动配置的"总开关" 3.小结 1.前言 作为Java开发者,你是否曾在传统Spring项目中反复编写@ComponentScan注解来指定Bean扫描路径?或者在集成第三方组件时,不得不手动通过@Import导入十几个配置类?

By Ne0inhk
Flutter 组件 short_uuids 适配鸿蒙 HarmonyOS 实战:唯一标识微缩技术,构建高性能短 ID 生成与分布式索引架构

Flutter 组件 short_uuids 适配鸿蒙 HarmonyOS 实战:唯一标识微缩技术,构建高性能短 ID 生成与分布式索引架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 short_uuids 适配鸿蒙 HarmonyOS 实战:唯一标识微缩技术,构建高性能短 ID 生成与分布式索引架构 前言 在鸿蒙(OpenHarmony)生态迈向万物互联、涉及海量离线资源标识、蓝牙广播载荷(BLE Payload)及二维码数据极限压缩的背景下,如何生成既能保留 UUID 强随机性、又能极大缩减字符长度的唯一标识符,已成为优化存储与通讯效率的“空间必修课”。在鸿蒙设备这类强调分布式软总线传输与每一字节功耗敏感的环境下,如果应用依然直接传输长度达 36 字符的标准 UUID,由于由于有效载荷溢出,极易由于由于传输协议限制导致数据截断或多次分包带来的延迟。 我们需要一种能够实现高进制转换、支持双向编解码且具备低碰撞概率的短 ID 生成方案。 short_uuids 为 Flutter 开发者引入了将标准 UUID 转化为短格式字符串的高性能算法。它利用

By Ne0inhk
Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案

Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 sw 的适配 鸿蒙Harmony 实战 - 驾驭高性能微服务路由架构、实现鸿蒙端 HTTP 流量语义分发与逻辑守卫方案 前言 在鸿蒙(OpenHarmony)生态的分布式业务网关、多端协同数据中转站以及需要实现极端细粒度接口管控的各种后端闭环应用开发中,“请求路由的执行效率与逻辑灵活性”是决定系统能否支撑起高并发访问请求的命门所在。面对包含上百个动态参数的 RESTful API 契约、需要针对鸿蒙手机、自研设备等不同终端执行差异化鉴权的复杂路由逻辑。如果仅仅依靠原始的 if-else 显式判定或性能低下的线性字符串匹配。不仅会导致路由分发的延迟随着接口数量增加而呈指数级上升,更会因为缺乏一套工业级的“语义化(Semantic)”路由映射规范。引发严重的服务逻辑归属混乱与权限越界风险。 我们需要一种“语义分发、匹配自洽”的路由艺术。 sw(在 Shelf 生态中常指高效的 Switch/Router 增强件)是一套专注于实现极致性能与

By Ne0inhk
springboot校园讲座管理系统--附源码09295

springboot校园讲座管理系统--附源码09295

摘  要 在当今时代,网络无处不在,影响着我们的生活、工作以及学习,其覆盖的范围十分广泛,现在人们经常通过网络获取信息,因为通过网络获取信息方便、快捷,只需要一台联网电脑就可以实现,因此为了给校园用户提供一个网络进行校园信息查询、交流的平台,开发了本校园讲座管理系统。 本校园讲座管理系统主要针对校园用户对校园信息的查询、校园讲座预约、管理方面开发的,目的是给校园用户提供一个便捷的交流环境,让校园用户随时随地,只要联网登录到系统当中就能够对校园信息进行查询、发布自己的观点进行交流等,解决了传统点对点的见面或者邮件交流方式的弊端,使其交流方式更加直接、快捷、广泛。 此校园讲座管理系统采用了springboot框架进行开发,采用Java语言,使用了MySQL这一数据库,主要实现用户对校园讲座预约、通知公告、讲座资源管理以及管理员管理的功能,满足校友与校友、学生与老师社交、管理的需求。 关键词:springboot;Java语言;校园讲座管理系统 ;MySQL Abstract In today's era, the network is everywhere

By Ne0inhk