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

【优选算法必刷100题】第021-022题(二分查找):山峰数组的的峰顶索引、寻找峰值

【优选算法必刷100题】第021-022题(二分查找):山峰数组的的峰顶索引、寻找峰值

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 21. 山峰数组的的峰顶索引 解法(二分查找): 算法思路: 二分查找解法代码(C++): 22. 寻找峰值 解法(二分查找): 算法思路: 二分查找解法代码(C++): 总结: 前言: 聚焦算法题实战,系统讲解三大核心板块:“精准定位最优解”——优选算法,“简化逻辑表达,系统性探索与剪枝优化”——递归与回溯,“以局部最优换全局高效”——贪心算法,讲解思路与代码实现,帮助大家快速提升代码能力 二分查找专题 21. 山峰数组的的峰顶索引 题目链接: 852. 山脉数组的峰顶索引 -

By Ne0inhk

PyTorch 2.6最新镜像:支持Python 3.13开箱即用

PyTorch 2.6最新镜像:支持Python 3.13开箱即用 你是不是也遇到过这样的情况:想写一篇关于PyTorch 2.6的深度评测文章,结果发现本地环境已经被各种项目“污染”得乱七八糟?不同版本的Python、混杂的依赖包、残留的缓存文件……这些都会严重影响测试结果的可复现性。作为技术作家,我们最怕的就是——今天跑通了,明天就报错;在这台机器上没问题,在另一台却处处是坑。 别担心,现在有一个简单又干净的解决方案:使用预置了PyTorch 2.6 + Python 3.13的纯净镜像环境。这个镜像不仅帮你省去繁琐的环境配置过程,还能确保你在完全一致、无干扰的系统中进行测试和写作,真正做到“一次运行,处处可信”。 本文将带你从零开始,一步步利用ZEEKLOG算力平台提供的最新镜像资源,快速搭建一个专为PyTorch 2.6评测设计的标准开发环境。无论你是刚接触AI开发的小白,还是需要稳定测试环境的技术写作者,都能轻松上手。我们会讲清楚这个镜像到底解决了什么问题、怎么一键部署、如何验证核心功能(比如torch.compile在Python

By Ne0inhk
在线浏览“秀人网合集”的新思路:30 行 Python 把封面图链接秒变本地可点图库

在线浏览“秀人网合集”的新思路:30 行 Python 把封面图链接秒变本地可点图库

用 30 行 Python 把秀人网公开合集“搬”进本地数据库 “秀人网”近日上线的新主题合集页采用前端渲染,数据通过 /api/v2/theme/list 接口一次性返回 JSON,无需模拟点击“加载更多”。接口无登录限制,但带 5 秒滑动窗口的 IP 频次校验:单 IP >30 次/分即返回 429。本文示范如何遵守 robots 协议、放缓速率,仅采集“公开可见”字段,并给出断点续抓、User-Agent 随机化、异常重试等常用技巧。 核心思路三步走: 分析列表接口:在浏览器 DevTools 里筛选 XHR,发现真实请求 URL

By Ne0inhk

Ubuntu玩转Python:从配置到实战全指南

好的,这是一份在 Ubuntu 环境下使用 Python 的完整指南: 在 Ubuntu 环境下玩转 Python:从环境配置到实战开发全指南 Ubuntu 是开发者喜爱的 Linux 发行版之一,与 Python 结合能提供强大且稳定的开发环境。本指南将带你完成从环境配置到实战开发的完整流程。 一、环境配置 1. 检查系统自带 Python * Ubuntu 通常预装了 Python。 * 查看输出,确认版本(如 Python 3.10.12)。python 命令可能指向 Python 2,建议始终使用 python3 和 pip3。 2. 安装 Python 开发工具包 3. 4. 使用虚拟环境(强烈推荐)

By Ne0inhk