
前一阵子我接手了一个内部的知识产权管理系统用 SpringBoot Vue 把专利、商标、软著这些档案管起来。做完之后最大的感受是这种系统门槛不高但坑都在“业务规则”上比如年费哪天该交、官文在谁手里、状态流转谁有权操作。本文就把这套系统的设计思路、核心模块、数据库表结构、权限模型、前后端实现和部署过程完整拆一遍适合准备做毕设的在校生、想自研内部系统的小团队也想帮刚接触前后端分离开发的朋友少走几个弯路。1. 项目到底要解决什么问题1.1 核心业务场景与用户痛点这套系统面向的并不是“卖软件”的通用办公系统而是研发型企业、高校课题组、代理机构内部的一套业务管理工具。实际场景很具体研发部门提交了一个发明专利流程管理员要撰写交底书、对接代理人财务要跟踪官费和代理费分管领导要看到每年知识产权的存量、授权率、预算执行情况。如果没有系统支撑我见过最典型的混乱状态是各种专利、商标信息分散在多个 Excel 表里交费截止日靠行政同事的备忘录离职交接时只有一串没有密码的共享文件目录。具体痛点可以归纳成四类。第一是“状态不可见”一个专利到底在“内部审核”“代理提交”“审查中”还是“已授权”只有经办人自己知道其他人问起来全靠口头沟通。第二是“期限无提醒”发明专利年费缴费、商标续展这种硬时限错过一次可能直接导致权利失效损失并不是几千块能覆盖的。第三是“费用对不上”申请费、年费、代理费记在好几个地方年底汇总的时候要人工对账效率极低。第四是“权限分不清”发明人应不应该看到代理费明细、财务能不能修改技术交底书这些边界如果不去建模系统上线之后一定会被业务人员吐槽权限要么太粗要么太死。1.2 功能需求梳理围绕上面这些痛点系统最终落地成六大功能模块。档案管理是基础负责专利、商标、著作权三种类型的登记、查询和详情查看每条档案关联申请人、发明人、代理机构等基础字段。流程管理负责状态流转比如从“草稿登记”推送到“内部审核”再由管理员标记为“已提交官方”每一步要记录操作人和时间。期限提醒模块是价值核心系统根据申请日或法定规则自动计算年费缴纳日、续展日在截止前 30 天和 7 天分别推送提醒。费用管理记录每一笔款项的金额、类型、缴纳状态和对应发票。文件卷宗模块则按案件归档交底书、受理通知书、授权证书扫描件等材料。最后还有统计报表和权限管理前者用于年度盘点后者用于控制不同角色的可见范围。1.3 角色权限拆解权限部分我用 RBAC 模型来设计。真正使用系统的角色大概有五类研发人员可以登记新案件、查看自己名下所有案件但不能修改费用数据流程管理员拥有最大业务权限负责案件信息维护、代理对接和状态流转财务人员只看自己权限范围内的缴费信息和发票录入分管领导只查看统计仪表盘和年度分析报表系统管理员负责用户、角色和字典数据维护。这套设计在代码里不复杂但因为角色边界和业务强相关要在需求阶段就说清楚不然后端接口的鉴权会反复改。2. 技术选型与整体架构2.1 为什么选 SpringBoot VueSpringBoot Vue 是当前中小型前后端分离项目里最稳妥的组合没有之一。后端 SpringBoot 让配置内嵌、打包发布、依赖管理都非常省心生态里要什么都有从数据库访问到消息通知再到任务调度基本不用从零造轮子。前端 Vue 上手曲线平缓组件化开发很适合表单密集型的管理系统第三方的组件库和后台管理模板也非常成熟拿来就能把整体框架搭起来。这套组合选型的好处不是“性能数据最优”而是团队招聘成本低、后续维护资料多、改造空间大一个项目从开发到交付的整个生命周期都相对可控。2.2 为什么不选其他方案在选择阶段我也考虑过另外几条路线。第一种是传统的单体模板渲染也就是后端直接返回 Thymeleaf 页面这种方式在纯内部系统里反而开发更快但缺点是前后端逻辑耦合太深后面如果要加移动端接口、做复杂的交互页面会非常难受。第二种是微服务拆分把用户、案件、费用、文件各拆一个服务这种架构在业务量只有几万数据的系统里完全是过度设计光服务间调用、分布式事务、链路追踪就能拖慢整个开发进度。第三种是选用低代码平台虽然能快速搭出增删改查界面但专利年费这种复杂业务规则很难在平台上表达真出了状况也不如自己掌握的代码灵活。权衡下来SpringBoot Vue 是功能、成本、可维护性三者平衡最好的选择。2.3 后端配套组件选型后端核心组件我按职责选型。安全认证用 Spring Security JWT不用 Session 的主要原因是系统后面要支持接口给外部程序调用JWT 无状态、跨域、多端的扩展性都更好。数据访问用 MyBatis-Plus单表 CRUD 基本不用写 SQL分页、逻辑删除、乐观锁这些功能都内置配合代码生成器效率很高。缓存用 Redis只用来存登录用户的 token 和验证码业务数据暂时不需要长期缓存减少一致性维护成本。任务调度用 Spring 自带的 Scheduled实现每天 8 点扫描一期到期提醒没引入 xxl-job 这种重框架因为单机版内部系统没必要。表格导出用 EasyExcel大数据量的情况下内存开销小。文件存储用的本地磁盘未来如果要上 NAS 或对象存储只需改一个接口实现。2.4 前端与部署方案前端我选择 Vue 3 Vite Element Plus。Vite 的开发调试速度比 Webpack 时代好了很多Element Plus 的表单组件、日期选择器、上传组件配合业务开发非常顺手。状态管理用 Pinia远比 Vuex 的写法简洁。HTTP 请求统一封装 Axios拦截器里做 token 注入和 401 统一处理。路由用 Vue Router结合后端返回的权限码做动态路由和按钮级权限控制。部署方案是前后端分离部署后端打成可执行 jar前端构建成静态资源交给 Nginx 托管同时对 /api 路径做反向代理。这套部署方案的好处是环境依赖少拷到哪台机器都能跑升级时前后端可以各自独立发布。整体技术栈汇总如下层次选型说明后端框架Spring Boot 2.7内嵌 Tomcat配置简化安全认证Spring Security JWT无状态 token 鉴权数据访问MyBatis-Plus单表免写 SQL分页插件缓存Redis存储登录态、验证码任务调度Spring Scheduled每日扫描到期提醒表格导出EasyExcel导出案件汇总报表前端框架Vue 3 Element Plus组件化 成熟后台模板构建工具Vite开发热更新快部署Nginx systemd前端静态资源 后端 jar3. 数据库设计核心表结构与权限模型3.1 设计原则知识产权管理系统的数据量不大但数据关系相对复杂设计时我遵循三条原则。第一是“以业务档案为中心”专利、商标、软著虽然类型不同但共同点是都有一份主档案费用、文件、期限提醒全部通过 case_id 关联到主表而不是各类型分开建独立流程这样统计和通用操作都能复用一套逻辑。第二是“状态宁可要枚举不要散文字”所有状态字段都用 int 类型存字典值并维护一张数据字典表前端展示时统一翻译避免出现“已授权”“授权”这样不统一的脏数据。第三是“金额账目表只做流水记录”每笔费用单独成行不做累计字段需要汇总时实时聚合杜绝并发更新累计值带来的数据不一致。3.2 核心表结构拆解系统核心表大概有八张我逐个说明设计思路。sys_user 用户表存放登录账号、密码BCrypt 加密、姓名、所属部门、状态。sys_role 角色表记录角色编码和名称。sys_permission 权限表是树形结构记录菜单、按钮、接口三种类型的权限点字段包括权限编码、父级 ID、权限类型、前端路由地址。ipr_case_info 是案件主表字段包括案件编号、案件标题、业务类型专利/商标/软著、申请日、申请人、发明人、代理机构、当前状态、技术领域、备注。这里有一个经验不要给三种业务类型各建一张表然后做视图合并后期按标题搜索的时候非常痛苦统一主表加类型字段的设计更利于功能扩展。ipr_fee_record 费用表记录 case_id、费用类型申请费/年费/代理费/复审费、金额、币种、缴费状态、应交日期、实缴日期、发票号。金额字段用 DecimalJava 对应 BigDecimal。ipr_deadline 期限提醒表比较特殊它既存计算出的日期也存提醒状态由定时任务每天扫描把待提醒的事件生成到站内通知表。ipr_file_record 文件表保存文件的本名、存储名、路径、大小、上传人避免直接用原始文件名做存储名防止中文路径和重名覆盖问题。最后是 operation_log 操作日志表记录操作人、模块、行为、请求参数、耗时和客户端 IP。这个表不是为了满足合规要求才做的而是解决“这个流程是谁改的、什么时候改的”这类业务纠纷的依据。3.3 状态字段设计状态流转是整个系统里最容易写乱的部分我统一用状态枚举类管理不放数据库字典表里。专利的完整状态是草稿、内部审核中、已提交官方、审查中、已授权、已驳回、已撤回、维护中。商标的常见状态是申请中、初审公告、异议中、已注册、已续展、已失效。每种状态之间的流转规则写在一个 Service 方法里例如从“审查中”只能流转到“已授权”或“已驳回”不能直接跳转到“维护中”。状态字段本身用 int 存储业务代码中通过枚举常量引用这样数据库可读性和代码可维护性同时得到保障。3.4 RBAC 权限模型落地权限模型采用经典的“用户-角色-权限”三张主表和两张关联表。用户任意多角色角色任意多权限登录后把权限码集合放入 JWT 里的声明中。后端再利用 Spring Security 的 PreAuthorize 注解在接口上做权限校验前端则根据权限码渲染菜单和按钮。这种做法的好处是权限判断集中在后端前端的按钮隐藏只是体验优化不会成为安全边界。4. 后端核心功能实现细节4.1 登录认证与 JWT 鉴权登录接口的处理流程是前端把用户名密码发给 /api/auth/login后端校验验证码和密码密码通过 BCrypt 校验后生成 token。生成 token 时我把 userId、用户名、角色集合、权限码集合放进去有效期设置成 2 小时。为了支持退出和强制下线登录成功时把 token 的 jti 存到 Redis并设置同样的过期时间退出接口删除 Redis 里的记录后续请求如果发现 token 合法但 Redis 里没有记录就判定为已失效。后端鉴权核心是自定义一个过滤器继承 OncePerRequestFilter在 Spring Security 过滤器链中加入。每次请求进来先从请求头 Authorization 中提取 Bearer token解析出用户信息后手动构造认证对象放入 SecurityContext。这里有一个特别容易踩的坑如果只是把 token 解出来就算鉴权通过用户被删除或角色被修改后旧 token 依然有效。所以此处必须查一下 Redis 中的会话记录同时从数据库加载最新的角色权限而不是完全信任 token 里的旧声明。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Resource private TokenService tokenService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { String token resolveToken(request); if (StringUtils.hasText(token) tokenService.validateToken(token)) { LoginUser loginUser tokenService.getLoginUser(token); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearer request.getHeader(Authorization); if (StringUtils.hasText(bearer) bearer.startsWith(Bearer )) { return bearer.substring(7); } return null; } }4.2 案件档案的增删改查实现案件登记的接口实现并不难但有三件事必须处理好。第一是案件编号的生成规则我采用“类型编码 年 月 序号”的组合方式比如 PAT-202506-001生成过程要加乐观锁防止多人同时登记时出现重复号。第二是创建和编辑的权限校验前端虽然已经根据按钮权限隐藏了入口但后端接口依然要加 PreAuthorize 校验确保只有流程管理员和相关角色能执行写操作。第三是详情查询的返回结构案件详情不仅包含主表基础信息还需要同时返回费用列表、文件列表和状态流转记录这就要求查询层做一次聚合避免前端为了拼数据多次请求接口。删除操作我用 MyBatis-Plus 的 TableLogic 逻辑删除而不是物理删除。重要业务档案一旦被物理删除后续追溯会非常麻烦。逻辑删除字段类型用 tinyint默认 0删除时置 1查询时框架自动拼接过滤条件。这样既不影响列表性能又能保留底账。4.3 期限提醒与定时任务期限提醒是这个系统的核心价值点实现思路分两步计算和扫描。计算在案件信息创建和状态变更时触发。专利类的缴费节点按申请日计算发明专利申请日满一年要缴第 2 年年费后续逐年递增具体每个国家阶段的费用标准可以做成一张费用规则表由管理员在系统里维护代码不用写死。商标类的续展是申请日起每 10 年一个周期到期前 12 个月进入可续展期到期后还有 6 个月的宽展期。软著则主要提醒证书领取时间和材料补正截止时间。计算完成后把待提醒事件插入 ipr_deadline 表状态为“未提醒”。扫描通过 Spring 的 Scheduled 注解实现系统每天上午 8 点扫描一次。查询条件是提醒日期在当天到未来 30 天之间并且提醒状态是“未提醒”的事件为每个事件生成一条站内消息同时把提醒状态更新为“已提醒”。这种设计能保证提醒不会重复发送如果系统停机错过了扫描恢复后补跑逻辑也能基于日期范围兜底。Component public class DeadlineNoticeTask { Resource private DeadlineService deadlineService; Scheduled(cron 0 0 8 * * ?) public void scanAndNotify() { ListIprDeadline deadlines deadlineService.findPendingRemind(LocalDate.now(), LocalDate.now().plusDays(30)); for (IprDeadline deadline : deadlines) { noticeService.pushMessage(deadline.getCaseId(), deadline.getEventType(), deadline.getDeadlineDate()); deadlineService.markReminded(deadline.getId()); } } }4.4 费用与年费管理费用管理最容易出问题的是精度。官方费用和代理费都是精确到分的金额如果用 double 类型累加会引入不可控的精度误差累计到年底对账的时候差异会非常明显。我统一用 BigDecimal并且金额字段传入实体时用字符串接收避免 JSON 反序列化过程中的精度丢失。所有金额展示在前端用过滤器统一格式化两位小数统计汇总则在后端完成前端不直接对金额做任何计算。费用的状态机也有讲究新建费用默认“待缴纳”财务人员核销到账后改为“已缴纳”开票后补充发票号作废则单独用“已作废”状态而不是直接删除记录。这样做的好处是每一笔钱的来龙去脉都有迹可循审计时会非常省心。4.5 文件与卷宗管理文件模块采用“原文件名 UUID”的存储策略。上传时先校验文件扩展名和大小限制防止上传 php 等危险文件然后把文件存储到按年分的目录下数据库记录原始文件名、存储文件名和文件大小。前端下载时根据记录里的存储文件名读取文件流用原始文件名设置响应头避免用户下载后看到一串 UUID。这里响应头要注意对中文文件名做 URL 编码处理否则部分浏览器下载文件名会乱码。为了限制单个文件占用过大磁盘我在配置里将上传上限设置为 50MB。技术文档扫描件一般不会超过这个大小如果有超大附件需求再单独走大文件上传通道比如分片上传。4.6 操作日志与审计操作日志模块用 AOP 统一记录。我定义了一个 OperationLog 注解标注在需要记录的 Controller 方法上通过切面在方法调用前后分别记录开始时间和结束时间把操作人、方法描述、请求参数、返回耗时写入日志表。这样做比在每个业务方法里手写日志代码干净很多。要注意的是请求参数中如果包含密码、旧 token 这类敏感信息写日志前要做脱敏否则日志表会变成新的安全风险点。5. 前端 Vue 实现要点5.1 工程初始化与路由设计前端工程用 Vite 创建 Vue 3 项目目录划分按模块views 放页面组件api 放接口定义stores 放状态管理router 放路由utils 放工具函数。路由设计采用静态路由 动态路由组合的方式。静态路由只包含登录页、404 页和布局框架动态路由在用户登录后根据后端返回的权限码生成通过 router.addRoute 挂载到根路由下。菜单渲染和路由挂载要保持一致的数据来源。后端返回的权限点带有父级 ID 和类型前端把它整理成树形菜单结构同时按路由路径映射到页面组件。这样做的效果是没有授权过的业务模块用户不仅看不到菜单入口直接手输 URL 也无法进入相应页面。5.2 Axios 封装与统一拦截Axios 封装是每个前后端分离项目的基建工程。新建一个 request.js创建 axios 实例设置 baseURL 为 /api请求拦截器里从 localStorage 读取 token 并注入 Authorization 头。响应拦截器做两件事一是当 HTTP 状态为 200 时直接返回业务数据二是当状态为 401 时清空本地登录信息并跳转登录页。另外我加了一层业务错误码处理比如后端返回 500 时前端统一弹出后端接口返回的错误消息不直接把浏览器原生错误抛给用户。5.3 表单与列表页的高效开发列表页几乎占了后台管理系统的一半工作量。我的做法是先封装一个通用的列表页组件接收“查询条件配置项”和“表格列配置项”通过配置驱动方式生成筛选表单、工具栏和表格。这样同一个项目里十几个页面的列表交互保持统一新页面只需新增一个配置文件。表单页同样封装成配置驱动字段的校验规则、联动逻辑、数据字典都写在配置对象里大幅减少重复逻辑。这块虽然前期有一定开发成本但项目越往后效率优势越明显。5.4 期限提醒看板期限提醒看板是给业务人员的核心工作台。页面上显示未来 30 天内需要处理的到期事件按紧急程度排序逾期未处理的事件置顶显示红色标志。点击事件可以跳转到对应案件的详情页进行缴费确认或状态更新。这个页面的数据来自后端 /api/dashboard/upcoming 接口返回截止日期、案件标题、事件类型、剩余天数。剩余天数由后端计算好前端不做时间差计算避免不同浏览器环境下时区偏差导致显示不一致。6. 部署与运维实践6.1 后端打包与进程管理后端使用 Maven 打包成可执行 jar打包命令是mvn clean package -DskipTests生成的 jar 部署到服务器后我用 systemd 配置成系统服务这样开机自启、崩溃重启、日志管理都比较方便。管理脚本也很简单重点是把启动参数中的 JVM 内存按服务器配置调好比如 -Xms512m -Xmx1024m避免默认值太大把服务器内存占满。生产环境的配置通过 application-prod.yml 拆分数据库地址、Redis 地址、日志级别都跟开发环境隔离打包时指定 profile 生效。6.2 前端构建与 Nginx 部署前端构建命令是 npm run build产物是 dist 目录。我把 dist 里的静态文件上传到服务器 /opt/frontend 目录然后用 Nginx 托管。Nginx 配置里有两点最关键一是 location / 指向静态资源目录二是 location /api/ 反向代理到后端服务同时附带头信息。server { listen 80; server_name your-domain; root /opt/frontend; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; 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 / { try_files $uri $uri/ /index.html; } }try_files 那句配置一定要加否则页面在非根路径下刷新会直接 404。前端路由是 history 模式刷新时 Nginx 需要把请求回退到入口 HTML。6.3 Docker Compose 编排如果目标服务器上装了 Docker我更推荐用 docker-compose 编排一套完整环境。编排内容包含 MySQL 8、Redis 7、后端服务、前端 Nginx 四个容器。数据库和 Redis 数据挂载到宿主机磁盘目录避免容器重建时数据丢失。后端镜像基于 eclipse-temurin:8-jre 构建启动命令里指定 Spring 配置和数据库连接参数。前端 Nginx 镜像用官方 nginx:alpine把打包好的 dist 覆盖到容器内静态目录并挂载 nginx.conf。这种方式的好处是环境可复制换一台服务器直接 docker-compose up 就能拉起整套系统。6.4 数据库备份策略知识产权系统的数据性质决定了备份不能省。数据库备份我用 mysqldump 每天凌晨全量备份一次保留最近 7 份同时每周再导出一份完整 Excel 汇总发给相关负责人做线下存档。日常注意检查备份文件是否可恢复我见过太多系统备份任务一直在跑但真到恢复的时候文件是坏的。所以备份之外每季度最好手动做一次恢复完成流程确保备份可用。7. 常见问题与排查技巧实录7.1 跨域问题排查开发环境下前端跑在 5173 端口后端跑在 8080 端口直接请求必然跨域。最好的解决方式不是后端开全局 CORS而是用 Vite 内置的代理转发。前端请求的 baseURL 是 /apidevServer 里配置 proxy 到 localhost:8080这样开发环境的浏览器看到的永远是同源请求。生产环境无需 CORS 配置因为 Nginx 已经做了同源反向代理。如果后端接口确实需要被第三方调用再单独开 CORS 白名单不推荐用 addCorsMappings 放行所有来源。7.2 事务失效的典型场景费用登记和案件状态更新如果放在同一个方法里需要加 Transactional 保证原子性。但事务注解有一个经典失效场景同一个类里的方法内部调用不会经过 Spring 代理导致注解不生效。比如 Service A 的 methodOne 调用了同类的 methodTwomethodTwo 上的 Transactional 不会起作用。解决办法是拆成不同的 Service Bean 互相调用或者把事务边界放到 Controller 调用的那一层。这个坑如果排查不仔细会出现“费用记录保存了但案件状态没更新成功”这种极难复现的问题。7.3 接口权限绕过前端隐藏按钮、隐藏路由都只是体验层面后端必须做权限校验。Spring Security 里把接口权限配置成注解形式每个写操作接口加上 PreAuthorize(hasAuthority(case:edit))这样即使有人直接调接口也无法越权。这里要注意权限点编码必须全局唯一命名用“模块:动作”的规范例如 case:add、fee:confirm、case:delete避免权限配置串了模块。7.4 大文件上传超时默认上传限制只有 1MB上传几十兆的扫描件会直接报错。需要在 application.yml 中修改 multipart 配置spring: servlet: multipart: max-file-size: 50MB max-request-size: 55MB同时 Nginx 层也要把 client_max_body_size 调大否则请求体过大时 Nginx 会直接返回 413。两层都要配只调 Spring 不调 Nginx 照样失败。7.5 时间字段的时区坑数据库存时间字段用 datetimeJava 实体用 LocalDateTimeJSON 序列化配置了统一格式。但有个容易翻车的点MySQL 连接串里如果没有指定 serverTimezone在容器化部署时默认时区可能不是东八区导致查询出来的时间比实际少 8 小时。连接串加 ?serverTimezoneAsia/Shanghai 能解决大部分问题同时前端展示历史日期时建议直接使用后端格式化好的字符串减少本地时区干扰。7.6 问题排查速查表现象可能原因解决办法登录后刷新页面 404前端路由 history 模式未配置回退Nginx 加 try_files接口调用返回 401token 过期或 Redis 会话被清除前端拦截 401 跳转登录上传附件报 413Nginx 请求体限制过小调整 client_max_body_size金额汇总对不上double 精度问题改用 BigDecimal定时任务未触发cron 表达式写错或时区不对检查 cron 和服务器时区修改角色后权限未变token 中权限码未更新登录时实时查库并刷新 token最后说一点个人体会这套系统从立项到上线前后大概两个月真正的难点不在写代码在于把业务规则翻译成可执行的逻辑。就像年费提醒听起来只是查个日期实际上要把费用规则表、状态变更时机、提醒去重、权限范围这几个环节串起来少一段都是错。我的建议是做这类管理系统之前先把状态流转图、角色职责表、核心字段清单三张纸写明白再动手建表写接口比边写边想能省一半返工时间。如果你正在做类似的项目希望这篇拆解能帮你把坑提前踩平。