拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot和Vue的研发项目管理系统设计与实践

基于Spring Boot和Vue的研发项目管理系统设计与实践

1. 立项背景与技术选型:为什么是 Spring Boot + Vue

说起研发项目管理系统,很多人第一反应是"这不就是 Jira 换个壳吗"。但真当你坐在团队里,发现需求文档散落在共享盘、排期全靠 Excel、每个人对"当前版本到底做到哪了"的回答都不一样的时候,就会明白:一个贴合自家流程的管理系统,比套用一个国际大厂的产品要顺手太多。

我当时的情况是团队二十来人,产品、设计、后端、前端、测试全挤在一个项目里,迭代节奏快,需求变更频繁。市面上的开源项目管理工具要么太重(安装部署一堆依赖),要么流程固定死板(自定义工作流要付费或写插件)。于是我们决定自研一套"基于Spring Boot + Vue的研发项目管理系统",核心目标只有一个:把需求、迭代、任务、缺陷、成员这五件事串起来,让进度可追踪、责任可回溯。

1.1 研发项目管理到底缺的是什么

先说一个反直觉的结论:多数团队缺的并不是"更多功能",而是一条清晰的状态流转链路。

我调研了一圈同事的使用习惯,发现大家真正高频使用的功能是:看某个需求属于哪个迭代、当前卡在谁手里、是否阻塞、缺陷有没有回滚。至于燃尽图、工时统计这种报表,只有管理者偶尔看一眼。所以系统设计之初,我就把"状态机"当成第一等公民来设计,而不是把"功能菜单"当成第一等公民。

具体来说,需求从"待评审"到"已排期",从"开发中"到"待测试",从"测试通过"到"已上线",中间还有"挂起"“已拒绝”等分支状态。每一个状态变更都要记录操作人、操作时间、变更原因。这套设计后来成为整个系统最被同事认可的部分,因为它把"谁在什么时候做了什么决定"完整沉淀了下来。

1.2 技术栈选型的现实考量

选型这件事,我见过太多人栽在"追新"上。Spring Boot 3.x 出来的时候,很多人立刻升上去,结果遇到 jakarta 命名空间迁移、Spring Cloud 组件版本对不齐、旧依赖不兼容等问题,光迁移就花了一周。我们最终选择Spring Boot 2.7.18作为后端基线,理由非常实际:

  • 2.7.x 是 Spring Boot 2.x 的最终版本,社区补丁最全,生态兼容性最稳。
  • 市面上大部分第三方 starter、老项目中沉淀的工具类,都以 2.x 为兼容目标。
  • 如果未来一定要升 3.x,2.7 的迁移路径也是官方重点维护的文档路线。

前端选 Vue 3 而不是 Vue 2,则更多是从组件生态和长期维护考虑。Vue 3 的组合式 API 对复杂业务状态的整理能力比 Vue 2 的选项式 API 强很多,而且 Element Plus、Ant Design Vue 这些组件库都已经全面转向 Vue 3。加上 Vite 的构建速度比 Webpack 快一个量级,前端开发的体验提升非常明显。

层面技术选型选择理由
后端框架Spring Boot 2.7.18生态成熟、排错资料多、兼容面广
前端框架Vue 3 + Vite组合式 API 更灵活,构建速度快
数据库MySQL 8.x团队熟悉,事务支持可靠
缓存RedisToken 黑名单、热点数据缓存
对象存储MinIO私有化部署,兼容 S3 API
后端权限Sa-Token / Spring Security + JWTJWT 无状态,配合拦截器实现鉴权
前端组件库Element Plus表单/表格生态完善,适合中后台

1.3 系统模块边界划分

我画系统模块图的时候,只给系统分了七个域,控制住了第一版的复杂度:

  1. 项目管理域:项目空间、成员角色、项目设置。
  2. 需求管理域:需求 CRUD、需求变更记录、附件关联。
  3. 迭代管理域:迭代创建、需求排期、迭代发布。
  4. 任务管理域:任务拆解、任务指派、工时登记。
  5. 缺陷管理域:缺陷提交、缺陷修复、回归验证。
  6. 统计报表域:迭代燃尽、成员负载、缺陷趋势。
  7. 系统管理域:用户、角色、菜单、操作日志。

这种划分不是按"数据库表"来的,而是按业务域来的。好处是前后端同学沟通时能共用一套语言:"这个功能属于缺陷管理域"比"这个功能改的是 defect 表和相关联的七八张表"要清晰得多。后端的包结构也严格按域划分,杜绝了随着迭代演进变成一锅粥的问题。

2. 后端落地:Spring Boot 的分层设计与核心机制

2.1 分层架构与自动装配原理

后端我采用的是经典的四层结构:Controller 层接收参数、Service 层处理业务、Mapper 层访问数据库、DTO/VO 做数据模型隔离。这里有个经常被忽略的细节:实体类(Entity)和视图对象(VO)必须分离。我见过很多项目图省事,直接拿数据库实体返回给前端,结果密码哈希、内部状态字段全暴露了,后续想加字段还得先迁移表结构。虽然在 Spring Boot 里可以通过 @JsonIgnore 逐个屏蔽字段,但根本解法还是建一层 VO,按前端需要来组装数据。

新人对 Spring Boot 的困惑主要集中在"为什么我什么都没配,它就能跑起来"。这其实是自动装配机制在起作用。以数据源配置为例,spring-boot-starter-jdbc 引入后,DataSourceAutoConfiguration会扫描 classpath 下的连接池实现,再结合application.yml里的spring.datasource.url等属性,自动创建好数据源 Bean。整个过程依赖三个东西:@EnableAutoConfiguration注解、META-INF/spring.factories中的配置类声明、条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean)。

理解这个机制对排错至关重要。比如你引入了一个第三方 starter,项目启动时报"Bean 找不到",那大概率是条件注解不满足——要么缺某个类,要么配置前缀写错了。我当时排查过一个 MinIO 连接失败的问题,现象是启动不报错,但一调用上传接口就 NoSuchBeanDefinition,最后发现是 starter 自动装配只在 classpath 里存在minio客户端类时才生效,而我的依赖坐标写错了包名。

2.2 需求-迭代-任务的数据模型

研发项目管理系统最核心的数据模型,就是需求、迭代、任务这三张表和它们的关系。

需求表我设计了这些关键字段:

  • requirement_code:需求编号,格式 "REQ-2024-001",用于跨部门沟通时快速引用。
  • title、description:标题和详细描述,描述存富文本,但入库前会做 XSS 过滤(这个后面专门讲)。
  • status:状态机字段,取值待评审、已评审、已排期、开发中、待测试、测试中、已上线、已挂起、已拒绝。
  • priority:优先级(P0-P3),排序时优先按优先级,再按创建时间。
  • owner_id:当前负责人,状态流转时更新。
  • iteration_id:关联迭代,可为空(表示尚未排期)。

迭代表相对简单:iteration_name、start_date、end_date、status。但有两个字段值得专门设计:goal(迭代目标)和retrospective(迭代复盘),前者让团队对齐方向,后者让复盘有迹可循。

任务表是需求的拆分。一个需求对应多个任务,每个任务有独立的负责人、预估工时、实际工时、状态。任务状态我简化成待处理、进行中、已完成、已取消四种,不搞太多分支,因为任务层面的状态太细反而增加填写成本。

这套模型建好之后,最关键的实现是状态机校验。我不会在 Service 里写一堆if (entity.getStatus() == 1 && targetStatus == 2)这种散装逻辑,而是用状态机配置表统一管理:

// 状态机配置:当前状态 -> 允许流转到的目标状态集合 public class RequirementStateMachine { public static final Map<String, List<String>> TRANSITIONS = Map.of( "待评审", Lists.newArrayList("已评审", "已挂起", "已拒绝"), "已评审", Lists.newArrayList("已排期", "待评审", "已挂起"), "已排期", Lists.newArrayList("开发中", "已挂起"), "开发中", Lists.newArrayList("待测试", "开发中", "已挂起"), "待测试", Lists.newArrayList("测试中", "开发中", "已挂起"), "测试中", Lists.newArrayList("已上线", "待测试", "开发中"), "已上线", Lists.newArrayList("开发中", "已挂起"), "已挂起", Lists.newArrayList("待评审", "已拒绝"), "已拒绝", Lists.newArrayList() ); }

这样每次变更状态时,只需查这张配置表校验合法性,新加状态也只需改配置不碰业务代码。上线以来,因为非法状态流转导致的数据错乱基本归零。

2.3 JWT + RBAC 权限模型的实现

权限这块我们采用的是标准的 RBAC 模型:用户-角色-权限三层。角色划分得很粗:项目管理员、开发、测试、访客。每个角色在项目空间内能操作的菜单和数据范围不同。

认证机制用的 JWT,登录成功后签发 token,前端每次请求把头部的Authorization: Bearer <token>带上。后端用拦截器统一解析:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request.getMethod().equals("OPTIONS")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } LoginUser user = JwtUtil.parseToken(token.substring(7)); if (user == null) { response.setStatus(401); return false; } // 将用户信息放入 ThreadLocal,Service 层可直接获取当前操作人 UserContext.set(user); return true; }

一个小坑提醒:JWT 的 token 无法在服务端主动失效。员工离职或者用户改密码后,旧 token 在有效期内依然能用。我们的解决方案是在 Redis 里存一份 token 黑名单,用户登出或者被禁用时把 token 的 jti(唯一 ID)写入黑名单。拦截器解析 token 后先查一次 Redis,命中黑名单直接拒绝。这个成本不高,但安全等级提升了一大截。

还有跨域问题。前后端分离之后,本地开发时前端跑 5173 端口,后端跑 8080 端口,必然产生跨域。开发环境我用 CorsRegistry 注册跨域规则,生产环境则全靠 Nginx 反向代理,把/api请求转发到后端容器,这样浏览器看到的永远是同源请求。很多人在生产环境配了跨域规则还抱怨"怎么又跨域了",其实就是没想清楚:既然是同源部署,压根就不该让跨域规则出现在生产代码里。

3. 前端工程化:Vue 3 项目组织与核心机制

3.1 脚手架与工程目录

前端这块我直接用 Vite 创建了 Vue 3 项目,目录结构按业务域划分:

src/ ├── api/ # 按后端域拆分的请求模块 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── composables/ # 组合式函数(usePagination、useFormDialog 等) ├── layouts/ # 布局组件(侧边栏、顶栏) ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── utils/ # 工具函数 └── views/ # 页面组件,按域分子目录

这里我想特别强调composables的价值。研发管理系统里大量出现"弹窗表单 + 表格刷新 + 分页重置"这个组合。如果不抽取成组合式函数,每个页面都得重复写一遍 loading、currentPage、pageSize、resetQuery 这些逻辑。我抽了一个usePagination,把表格加载、搜索条件、分页参数全部封装起来,业务页面只需要传一个 fetchList 回调。三个页面做下来,前端代码量直接少了三分之一。

3.2 Axios 封装与请求协同

Axios 封装是前端最基础也是最重要的基建。我在utils/request.ts里做了三件事:请求拦截器注入 Token、响应拦截器统一处理业务码、错误处理提示。

// 响应拦截器中统一处理业务异常 service.interceptors.response.use( (response) => { const res = response.data; if (res.code === 200) { return res; } if (res.code === 401) { // Token 过期,跳转登录页 router.push({ path: '/login' }); return Promise.reject(new Error('登录已过期')); } ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); }, (error) => { ElMessage.error(error.message || '网络异常'); return Promise.reject(error); } );

关于"多个请求同时返回 401 导致反复跳转登录页"的问题,我的做法是加一个isRedirecting标记,已经跳转过的就不再触发第二次。这个细节不写下来的话,实际运行时很容易出现登录页来回闪的情况。

3.3 动态路由与权限控制

权限控制不光是后端的事,前端菜单必须根据当前用户的角色动态生成。我采用的是"后端返回菜单树 + 前端动态注册路由"的方案。

后端在登录时返回该用户可见的菜单列表,每个菜单项带有路由路径、组件路径、图标等信息。前端拿到菜单树后,通过router.addRoute动态注册。

function addDynamicRoutes(menus: MenuItem[]) { menus.forEach((menu) => { if (menu.component) { router.addRoute({ path: menu.path, name: menu.name, // 这里使用 import.meta.glob 批量加载 views 下的组件 component: viewModules[`/../views/${menu.component}.vue`], }); } if (menu.children?.length) { addDynamicRoutes(menu.children); } }); }

动态路由有一个老大难问题:刷新页面后路由会丢失,因为 Pinia 里的菜单数据清空了。解决思路是把用户信息和菜单数据持久化到 localStorage,路由守卫里每次跳转前检查router.hasRoute和目标路由是否已注册,如果没有就先执行 addDynamicRoutes 再放行。这个方案我用了很久,稳定可靠。

另外,路由守卫里还必须包含一个按钮级权限的检查。我只在菜单级做了角色过滤,按钮级权限(比如"删除需求"按钮只有管理员能看到)让后端在接口层面做校验,前端通过自定义指令v-permission="['admin']"控制显隐。前端控制按钮显隐纯粹是交互优化,真正的安全边界永远在后端。

3.4 周边场景:Electron 壳与流媒体预览

热词里有人问 Electron 的主渲染进程 IPC 通信和 Vue 有没有关系。这里解释清楚:Electron 是桌面端的容器,Vue 只是跑在 Electron 渲染进程里的网页应用。主进程负责创建窗口、调用系统能力,渲染进程负责页面渲染,两者通过ipcMain和ipcRenderer通信。换句话说,Vue 本身不感知 Electron,Electron 也不关心你用的是 Vue 还是 React。我们系统里确实有同事想打包一个桌面端,方便测试同学不用开浏览器,做法就是先构建出静态文件,再用 Electron 加载,IPC 用来做"窗口最小化到系统托盘"这种原生交互。

流媒体预览场景,我们主要用于存储和播放产品演示视频。为了兼容不同网速环境,视频转码成 m3u8 切片后用 HLS 协议播放。Vue 端用hls.js这个库,几行代码就能接好:

import Hls from 'hls.js'; function playM3u8(videoEl: HTMLVideoElement, url: string) { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(videoEl); } else { // 部分 Safari 原生支持 HLS videoEl.src = url; } }

需要提醒的是,m3u8 播放涉及跨域请求,MinIO 或 Nginx 要给视频文件所在路径配置 CORS 和正确的 Content-Type。否则你会遇到"文件能下载但播放器黑屏"的诡异问题。

4. 高频集成场景与配置陷阱

4.1 文件上传与 PDF 安全处理

研发管理系统里文件上传是个高频功能:需求附件、缺陷截图、接口文档、测试报告。我们用 MinIO 做对象存储,上传接口走预签名 URL 模式:前端先向后端请求一个上传凭证,拿到之后直接往 MinIO 传文件,这样文件内容不经过应用服务器,减轻后端带宽压力。

MinIO 与 Spring Boot 集成时有个常见坑:bucket 权限设为 private,但下载时要生成预签名 URL。有人为了省事把 bucket 直接设成 public,导致任意人拿到 URL 就能浏览所有附件。我推荐的做法是:

  1. bucket 全部 private;
  2. 后端提供GET /api/files/{fileId}接口,内部校验用户是否有该文件关联业务对象的访问权限;
  3. 校验通过后,后端生成一个有效期 5 分钟的预签名 URL,重定向到 MinIO。

PDF 上传的安全则要单独讲。很多人以为 PDF 就是"不可执行的文档",其实 PDF 文件里可以嵌入 JavaScript、外部对象、超链接等,浏览器打开恶意 PDF 时可能触发 XSS。所以全局过滤器处理上传请求时,不能只对表单字段做 HTML 转义,对文件本身要做类型校验和内容扫描:

  • 通过文件头(Magic Number)校验真实类型,前端伪造的 Content-Type 一律不信任。PDF 的文件头是%PDF,图片的 JPEG 是FF D8 FF,PNG 是89 50 4E 47。
  • 对 PDF 做内容级过滤,至少要做到移除文档中的 JavaScript 动作和嵌入的可执行对象。Java 生态可以用 PDFBox 重新解析并重写文档,把有风险的 Action 清掉后再保存。
  • 如果业务允许,最好在文件上传后做一次隔离预览,前端预览组件不要直接使用文件原始 URL,而是通过后端代理带上鉴权头。

4.2 XSS 全局过滤器的设计边界

热词里提到"Spring Boot 项目全局过滤器处理上传 PDF 文件时 XSS 攻击",这其实涉及一个容易混淆的点:XSS 过滤器应该处理的是文本字段,而不是二进制文件。

我见过不少项目直接在过滤器里把request.getParameter拿到的内容全部做 HTML 转义,然后正则替换<script>标签。这个思路有两个问题:

  1. 富文本编辑器提交的内容含有合法的 HTML 标签(如<p>、<b>、<img>),无脑转义会把富文本变成一堆乱码。
  2. 对 multipart 文件表单做全局字符串替换,可能会把文件的二进制内容破坏掉,导致上传的文件无法正常打开。

正确做法是区分场景:对普通 JSON 请求,用一个XssRequestWrapper包装请求体,把<script>、javascript:、onerror=这类高危模式做转义;对富文本字段,采用白名单策略,只保留p、br、strong、a、img等安全标签,其余标签全部剥离。对 multipart 请求,过滤器只做文件类型和大小校验,不碰报文主体内容。

具体实现时,我写了一个XssFilter继承OncePerRequestFilter,对 Content-Type 做分流:

public class XssFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String contentType = request.getContentType(); if (contentType != null && contentType.contains("multipart/form-data")) { // 文件上传请求:只做大小和类型检查,不做内容改写 chain.doFilter(request, response); return; } // JSON / 表单请求:包装成 XssRequestWrapper 做过滤 chain.doFilter(new XssRequestWrapper(request), response); } }

核心原则是:过滤器层只做通用防护,业务字段级别的特殊校验放在 Controller 的参数校验里做。

4.3 搜索增强:HanLP 在需求检索中的应用

研发管理系统里有一个被严重低估的需求:搜索。用户不记得需求编号,只记得"上次讨论过一个关于登录超时的优化"。数据库的LIKE '%超时%'能解决问题,但遇到"登录过期"“会话失效”这些同义词就抓瞎了。

我们引入了 HanLP 分词库,在需求创建和编辑时对标题和描述做分词,把分词结果存入单独的requirement_tags表,搜索时先对用户输入做同样的分词,然后匹配标签表。HanLP 在 Spring Boot 里的集成本质上就是引入依赖后调用分词 API:

List<String> segments = HanLP.segment(text).stream() .map(term -> term.word) .filter(word -> word.length() > 1) // 过滤单字 .collect(Collectors.toList());

需要说明的是,HanLP 的标准分词依赖数据包较大,首次加载可能需要几秒钟,而且内存占用不小。如果项目规模不大、表只有几千条,直接用 MySQL 的全文索引(ngram parser)也可以。HanLP 的优势在于同义词扩展和自定义词典,比如你可以把"需求"“功能”"特性"配成同义词,让搜索召回更聪明。

4.4 消息解耦:ActiveMQ 的任务通知场景

任务被指派、需求状态变更、缺陷被认领这三类事件,都需要通知相关人员。一开始我直接在 Service 里调通知接口,结果需求变更时通知逻辑串在业务逻辑里,一个地方改通知模板就得重新部署整条业务线。后来引入 ActiveMQ,把通知事件发到队列,异步消费处理:

// 需求状态变更后发送事件 jmsTemplate.convertAndSend("queue.notify.requirement", new RequirementNotifyEvent(requirementId, oldStatus, newStatus, operatorId)); // 消费者处理邮件/站内信通知 @JmsListener(destination = "queue.notify.requirement") public void onRequirementNotify(RequirementNotifyEvent event) { List<Long> userIds = requirementService.findConcernedUsers(event.getRequirementId()); notifyService.send(userIds, buildMessage(event)); }

用消息队列的核心收益不是"快",而是把非核心链路从主流程里剥出去。就算通知服务挂了,需求状态照常变更,消息堆积在队列里,恢复后消费端补发即可。这个思路同样适用于操作日志、统计报表等场景。

ActiveMQ 集成时最容易出问题的是序列化机制。默认情况下 ActiveMQ 支持ObjectMessage,但跨语言、升级兼容性差,我统一改成 JSON 字符串消息,生产消费双方通过 DTO 类来约束消息结构,维护成本低很多。

5. Docker 化部署与运行维护

5.1 前后端镜像构建技巧

部署这块,我们直接走容器化。后端镜像的精髓是multi-stage build:

# 第一阶段:Maven 构建 FROM maven:3.8-openjdk-11 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第二阶段:精简运行时镜像 FROM openjdk:11-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

这里有个细节:先COPY pom.xml并执行dependency:go-offline再复制源码,是为了利用 Docker 的层缓存。只要 pom.xml 没变,依赖层就不会重新下载,本地开发反复构建镜像时能省下大量时间。

前端镜像采用 Nginx 托管静态文件:先用 node 容器构建,再把 dist 目录复制到 nginx 镜像里。有一点容易忽略:Vite 默认的构建资源路径是相对路径,如果前端代码里写死了base: '/',部署到 Nginx 子路由或者加了前缀的路径下,页面全白。我建议构建时把base配置为环境变量,CI 里根据部署环境传入。

5.2 docker-compose 编排与 Nginx 配置

整套系统我用一个 docker-compose.yml 编排:

  • mysql:数据库,数据卷挂载到宿主机。
  • redis:缓存,带密码访问。
  • minio:对象存储。
  • backend:Spring Boot 应用,环境变量注入数据库地址、Redis 地址、MinIO 地址。
  • frontend:Nginx 容器,依赖 backend 服务。

Nginx 配置的核心是反向代理:

server { listen 80; server_name pm.example.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://backend:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /file/ { proxy_pass http://minio:9000/; proxy_set_header Host $host; } location / { try_files $uri $uri/ /index.html; } }

try_files $uri $uri/ /index.html这行是 Vue Router 的 history 模式核心配置——它把所有非 API 请求都回退到 index.html,由前端路由接管。很多部署事故都是少了这一行,导致刷新二级路由页面时直接 404。

5.3 线上问题定位:反编译排查

容器化部署之后,代码在服务器上跑着,本地又因为各种原因没有保留对应版本的源码,这时候线上出了 bug,怎么定位?我有过两次不得不靠反编译来解决问题的经历,这里分享一个标准流程:

  1. 从镜像仓库把对应版本的 jar 包拉下来。
  2. 用 JD-GUI 或 CFR 打开 jar 包,定位到报错堆栈对应的类。
  3. 反编译后对比本地当前代码,重点看字段名、方法名、日志输出、条件分支的差异。
  4. 根据差异确认是版本不一致还是代码逻辑问题。

CFR 是命令行工具,更适合集成到排查脚本里:

java -jar cfr.jar app.jar --outputdir ./decompiled --silent true

反编译的代码可读性虽然不如原版,但足够看清控制流和关键字段含义。说句实在话,这个问题最好的解决方式还是 CI 流水线里打完包自动归档源码版本号,做到 jar 包与 git commit 一一对应。反编译是兜底方案,不是常规手段。

6. 复盘与建议:版本、踩坑与长期维护

最后聊聊我在开发维护这套系统过程中踩过的一些坑,以及沉淀下来的几条判断标准。

版本依赖的管理原则是"宁旧勿新,宁定勿浮"。Spring Boot 2.7.18 是 2.x 的最终补丁版,Vue 3 我锁在某个已验证过的 3.4 小版本上。不要图新鲜随手升 Minor 版本,升完你会发现 Element Plus 的样式变了、Vite 的插件不兼容了,一顿排查下来半天没了。真要升级,也走单独分支验证,而不是在主分支上边改边升。

全局过滤器这类横向组件,尽量少做业务判断。XSS 过滤器就只管转义和白名单,不要在里面顺手做了登录校验、日志记录、接口耗时统计。一次把所有横切逻辑塞进同一个过滤器,看起来"省事",实际上每次需求变更都要动过滤器,风险极高。正确的做法是拆分多个过滤器,按执行顺序排列,职责单一。

数据库表结构变更要尽早引入迁移工具。我们这个系统早期是手工执行 SQL 脚本,迭代到第三个月就乱了,说不清哪个环境执行到哪个版本。后面引入 Flyway,把所有建表和变更脚本纳入版本管理,才彻底解决。这个建议对任何规模的系统都适用,别觉得"团队小用不上"。

前端状态管理不要过度设计。Pinia 的 store 我只放三类数据:登录用户信息、系统配置信息、跨页面共享的临时数据(比如需求筛选条件)。列表页的数据一律放在组件内部管理,刷新页面就能重置,反而更符合直觉。滥用全局状态会让调试变得非常痛苦,因为每个页面都从 store 读数据,出 bug 时根本不知道数据是哪一步被改掉的。

最后分享一个我坚持了很久的开发习惯:每次改动核心状态机、权限逻辑或部署配置时,我都会写一条变更记录放在docs/目录下。不需要很长,三五行,说清楚"为什么改、影响范围、验证方式"就够。一年下来,这份文档就是团队最可靠的知识资产,远比任何代码注释都管用。

如果你想在自己团队复制这套系统,我的建议是先从需求域和迭代域做起,这两个域覆盖了研发管理最核心的链路。等工作流跑顺了,再逐步增加报表、文件存储、消息通知这些增强能力。不要妄想第一个版本就交付一个大而全的产品,稳扎稳打地迭代,才是这类系统能够长期存活的关键。

返回列表