1. 项目背景与核心价值
最近在整理技术仓库时,翻出一个去年为企业客户开发的web项目申报管理系统。这套系统采用SpringBoot+Vue的前后端分离架构,配合MyBatis和MySQL数据库,实现了项目全生命周期的数字化管理。经过半年多的实际运行检验,系统稳定性和功能性都得到了验证,今天就把这个项目的技术实现细节和踩坑经验分享给大家。
这类系统在科研院所、高校实验室和企业研发部门都有广泛需求。传统纸质申报流程存在效率低下、进度不透明、材料易丢失等问题。我们开发的系统实现了:
- 在线填报与自动格式校验
- 多级审批工作流
- 项目进度可视化跟踪
- 文档版本智能管理
- 数据统计分析看板
2. 技术架构解析
2.1 整体架构设计
系统采用前后端分离架构,后端基于SpringBoot 2.7.x,前端使用Vue 3 + Element Plus。这种架构选择主要基于:
- 开发效率:SpringBoot的自动配置和起步依赖大幅减少XML配置
- 性能考量:Vue3的Composition API比Options API更节省内存
- 维护成本:Element Plus组件库成熟度高,二次开发方便
技术栈全景图:
code复制[前端]
Vue3 + Vue Router + Pinia
Element Plus + ECharts
Axios + WebSocket
[后端]
SpringBoot 2.7.x
Spring Security + JWT
MyBatis-Plus 3.5.x
Hutool工具集
Lombok
[数据层]
MySQL 8.0
Redis 6.x
2.2 关键技术实现
2.2.1 动态表单引擎
申报表单需要支持不同项目类型的差异化字段。我们开发了基于JSON Schema的动态表单引擎:
java复制// 表单配置示例
{
"formId": "project_apply",
"fields": [
{
"fieldName": "project_name",
"label": "项目名称",
"type": "input",
"rules": [
{"required": true, "message": "必填项"},
{"max": 50, "message": "不超过50字符"}
]
},
{
"fieldName": "project_type",
"label": "项目类型",
"type": "select",
"options": [
{"label": "基础研究", "value": 1},
{"label": "应用研究", "value": 2}
]
}
]
}
2.2.2 工作流引擎
采用Activiti 7.x实现多级审批流程,关键配置:
xml复制<process id="project_approval" name="项目审批流程">
<startEvent id="start"/>
<userTask id="department_approve" name="部门审批"/>
<userTask id="expert_review" name="专家评审"/>
<exclusiveGateway id="gateway"/>
<userTask id="final_approve" name="最终审批"/>
<endEvent id="end"/>
<!-- 省略流程连线 -->
</process>
3. 数据库设计要点
3.1 核心表结构
sql复制CREATE TABLE `project_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`project_code` varchar(32) COMMENT '项目编号',
`project_name` varchar(100) NOT NULL,
`project_type` tinyint NOT NULL COMMENT '1-基础研究 2-应用研究',
`applicant_id` bigint NOT NULL,
`apply_time` datetime NOT NULL,
`budget` decimal(12,2) DEFAULT NULL,
`status` tinyint DEFAULT 0 COMMENT '0-草稿 1-待审核...',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`project_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `approval_flow` (
`id` bigint NOT NULL AUTO_INCREMENT,
`project_id` bigint NOT NULL,
`approver_id` bigint NOT NULL,
`approval_result` tinyint COMMENT '1-通过 2-驳回',
`approval_comment` varchar(500),
`approval_time` datetime,
PRIMARY KEY (`id`),
KEY `idx_project` (`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 性能优化实践
-
索引策略:
- 为所有外键字段建立普通索引
- 高频查询条件建立联合索引
- 使用覆盖索引减少回表
-
查询优化示例:
java复制// 错误写法 - N+1查询问题
List<Project> projects = projectMapper.selectList(queryWrapper);
projects.forEach(p -> {
User user = userMapper.selectById(p.getApplicantId());
p.setApplicantName(user.getName());
});
// 正确写法 - 联表查询
@Select("SELECT p.*, u.name as applicant_name " +
"FROM project_info p LEFT JOIN user u ON p.applicant_id = u.id " +
"${ew.customSqlSegment}")
List<ProjectVO> selectProjectList(@Param(Constants.WRAPPER) Wrapper<Project> wrapper);
4. 前后端交互设计
4.1 API规范
采用RESTful风格设计,部分关键接口:
| 接口说明 | 请求方式 | 路径 | 参数示例 |
|---|---|---|---|
| 项目分页查询 | GET | /api/projects | page=1&size=10&name=xxx |
| 项目详情 | GET | /api/projects/ | - |
| 提交项目 | POST | /api/projects | JSON body |
| 启动审批流程 | POST | /api/projects/{id}/start | - |
4.2 文件上传方案
采用分片上传解决大文件问题:
javascript复制// 前端实现
const uploadFile = (file) => {
const chunkSize = 5 * 1024 * 1024 // 5MB
const chunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumber', i + 1)
formData.append('totalChunks', chunks)
formData.append('identifier', file.uniqueIdentifier)
await axios.post('/api/upload', formData)
}
}
5. 部署与运维实践
5.1 生产环境部署
推荐使用Docker Compose部署:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
5.2 性能监控配置
SpringBoot Actuator + Prometheus + Grafana监控方案:
properties复制# application.properties
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.metrics.export.prometheus.enabled=true
6. 常见问题解决方案
6.1 跨域问题
前后端分离常见问题解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
6.2 事务失效场景
MyBatis事务管理注意事项:
- 确保方法被Spring代理(public方法)
- 避免自调用问题
- 正确配置事务传播行为:
java复制@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void submitProject(Project project) {
// 业务逻辑
}
7. 安全防护措施
7.1 接口安全
- JWT令牌实现:
java复制public class JwtTokenUtil {
private static final String SECRET = "your-secret-key";
public static String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
// 省略验证逻辑...
}
7.2 SQL注入防护
MyBatis防注入最佳实践:
- 使用#{}替代${}
- 避免直接拼接SQL
- 使用XML配置时的注意事项:
xml复制<!-- 安全写法 -->
<select id="selectByCondition" resultType="Project">
SELECT * FROM project_info
WHERE project_name LIKE CONCAT('%', #{name}, '%')
</select>
<!-- 危险写法 -->
<select id="selectByCondition" resultType="Project">
SELECT * FROM project_info
WHERE project_name LIKE '%${name}%'
</select>
8. 项目扩展方向
- 移动端适配:开发基于Uniapp的跨平台应用
- 智能推荐:基于历史数据推荐相似项目模板
- 区块链存证:关键审批环节上链存证
- 多租户支持:SAAS化改造
这套系统在实际开发中最大的收获是:复杂业务系统的设计必须从领域模型出发,先理清业务边界和核心流程,再考虑技术实现。我们在第一版开发时过于关注技术细节,导致后期频繁调整领域模型,付出了不少重构成本。