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

Flutter 三方库 git_hooks 鸿蒙强干预研发质量审核截断防线设防适配解析:依托钩子拦截引擎封锁全域代码递交链路建立极强合规化审计审查防火墙斩断-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 git_hooks 鸿蒙强干预研发质量审核截断防线设防适配解析:依托钩子拦截引擎封锁全域代码递交链路建立极强合规化审计审查防火墙斩断-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 git_hooks 鸿蒙强干预研发质量审核截断防线设防适配解析:依托钩子拦截引擎封锁全域代码递交链路建立极强合规化审计审查防火墙斩断技术债堆砌 前言 在 OpenHarmony 的大规模团队协作中,代码质量是团队的生命线。如果没有有效的约束,不符合规范的代码(甚至是无法通过静态分析的代码)会轻易地通过 git commit 进入代码库,导致 CI 构建频繁失败。git_hooks 库为 Flutter 开发者提供了一种轻量级的脚本化方案,可以在 Git 的关键生命周期(如提交前、推送前)自动运行检查。本文将带大家在鸿蒙端实战适配该库,夯实自动化工程的地基。 一、原直线性 / 概念介绍 1.1 基础原理/概念介绍 git_hooks 的核心逻辑是基于 Git

By Ne0inhk

完全免费!用阿里开源 CoPaw 养一只属于自己的 AI 小助理(魔搭启动,亲测有效)

先说一个小插曲:前几天我写了一篇介绍 Maxclaw 的文章,当时还是免费的,结果文章发出去没多久,Minimax 就悄悄改了规则,变成 39 元一个月起步了。当然,39 元其实也不贵——毕竟你去闲鱼搜"openclaw 代安装",随便一个人工服务都要 50 块往上走。但既然有完全免费的方案,为什么不用呢? 今天这篇,就给大家介绍一个我亲自跑通的、完全免费的方案:用阿里开源的 CoPaw,在魔搭创空间里一键启动,服务器免费,Token 每天 2000 次免费调用,不用装任何本地环境,浏览器打开就能用。 CoPaw 是什么?先用一分钟搞清楚 很多人第一次听到 CoPaw 这个名字,会以为是某种宠物应用。其实它的全称是 Co Personal Agent Workstation,是阿里

By Ne0inhk
Flutter 组件 actions_toolkit_dart 适配鸿蒙 HarmonyOS 实战:自动化套件方案,构建 GitHub Actions 深度集成与跨端流水线治理架构

Flutter 组件 actions_toolkit_dart 适配鸿蒙 HarmonyOS 实战:自动化套件方案,构建 GitHub Actions 深度集成与跨端流水线治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 actions_toolkit_dart 适配鸿蒙 HarmonyOS 实战:自动化套件方案,构建 GitHub Actions 深度集成与跨端流水线治理架构 前言 在鸿蒙(OpenHarmony)生态迈向全球化开源协作、涉及极大规模的跨端 CI/CD 流水线构建、多机型自动化兼容性测试及严苛的代码准入控制背景下,如何实现一套既能深度对接 GitHub Actions 核心底脚(Toolkits)、又能提供原生 Dart 编程感且具备工业级日志输出与状态管理的“自动化控制基座”,已成为决定应用研发迭代频率与交付质量稳定性的关键。在鸿蒙项目这类强调多模块(HAP/HSP)并行构建与分布式证书签名校验的环境下,如果 CI 脚本依然依赖大量零散的 Shell 拼接,由于由于环境变量的微差异,极易由于由于“脚本不可维护”导致鸿蒙应用在自动化发布环节频繁由于由于故障导致阻塞。

By Ne0inhk

LFM2.5-1.2B-Thinking惊艳效果展示:Ollama本地运行下长文本推理

LFM2.5-1.2B-Thinking惊艳效果展示:Ollama本地运行下长文本推理 在本地设备上运行强大的AI模型,曾经是科幻电影中的场景。如今,随着LFM2.5-1.2B-Thinking模型的发布,这一切变成了现实。这个仅有12亿参数的"小模型"却拥有令人惊叹的长文本推理能力,真正实现了"高质量AI装入口袋"的愿景。 1. 模型核心能力概览 LFM2.5-1.2B-Thinking是专为设备端部署设计的新型混合模型,它在LFM2架构基础上进行了深度优化。这个模型最大的亮点在于:用极小的体积实现了接近大模型的性能表现。 1.1 技术特点解析 LFM2.5系列通过扩展预训练和强化学习进行了全面优化。预训练数据量从10T token扩展至28T token,采用了大规模多阶段强化学习训练方式。这意味着模型在保持小巧体积的同时,获得了更丰富的知识储备和更强的推理能力。 核心优势对比: 特性传统大模型LFM2.5-1.2B-Thinking参数量70亿+12亿内存占用4GB+<1GB推理速度较慢极快(AMD CPU上239 tok/

By Ne0inhk