资讯动态

Spring Boot + Vue协作机器人门户开发:从表设计到部署全记录

发布时间:2026/10/7 4:32:07 来源:尧图企业网站定制
去年底接到一个协作机器人厂商的项目要帮他们把面向客户的门户网站系统搭起来内部项目代号就叫 058。一开始以为就是做个产品展示官网真动手拆需求才发现协作机器人门户要管的内容比普通企业站复杂得多产品系列、型号参数、规格书 PDF、三维模型、SDK 包、认证证书还有公告、下载统计、后台权限整个就是一个小型内容中台。技术栈没有悬念Spring Boot Vue后端一个可执行 jar前端构建完直接放进去这样公司在现场部署时不用折腾 Node 环境。这篇记录会从需求拆分、表设计、接口定义、前端落地到打包上线全过程把选型逻辑和踩坑点讲透给准备做毕设或者公司内部门户的同学一个参考。1. 为什么这种门户最后都落在 Spring Boot Vue而不是更花哨的方案1.1 门户到底要管多少东西我一开始把协作机器人门户想简单了以为只是做几个静态页面真正梳理完需求发现至少要管四类内容产品、资料、公告、用户行为。产品不只是放几张图而是要区分系列、型号、额定负载、工作半径、重复定位精度、介绍视频、规格书甚至还有三维模型资料要区分使用手册、CAD 模型、SDK 压缩包、认证证书每个资料还要和具体产品型号关联并且能记录下载次数公告要有置顶、发布状态还要支持图文混排的编辑用户行为里最核心的是下载统计和预览记录这些数据之后要支撑销售和市场。把这些需求列成表格会非常直观模块使用对象核心动作后台能力产品门户游客/客户浏览、筛选、查看参数上下架、排序、参数维护文档中心注册用户/客户预览 PDF、下载资料包上传、版本管理、下载统计公告与新闻游客阅读、分享富文本编辑、置顶、定时发布系统管理管理员权限管理、内容审核用户、角色、菜单、操作日志这个清单出来之后基本就能判断项目属于什么量级了它不是零交互的官网但也远没到要做消息队列和微服务的程度。1.2 为什么不用 Django、Next.js 或者现成 CMS市面上有很多内容管理方案比如 Strapi、Django Admin。只做内容发布这些方案一天就能跑起来。但机器人产品资料的下载要统计、要关联产品型号、要控制某些资料只给登录客户看这些自定义逻辑在 CMS 里要么写插件要么上钩子绕一圈最后还是回到业务代码。用 Spring Boot Vue 的好处是整个链路都在自己手里。Spring Boot 提供标准的 REST 服务、事务和权限控制Vue 负责交互和路由前后端通过接口契约协作。项目边界清晰新人接手也能顺着接口文档慢慢看后期加权限、加报表、接微信公众号都是增量开发不用推翻重来。另外有一点容易被忽略这套系统交付之后维护的人不一定学过前端工程化。Vue 项目说白了就是常规的 HTML、CSS、JavaScript 组件化比起上手 Next.js 的 SSR 概念团队续命成本低得多。1.3 版本选型直接影响后面的坑动手前我定的是 Spring Boot 3.2.x Java 17 MyBatis-Plus MySQL 8 Vue 3 Vite。为什么不选 2.x新项目没必要再用 javax 命名空间Spring Boot 3 已经是默认选项。但这里有个现实问题——网上大量教程还是 2.x 时代的代码复制过来会把javax.servlet编译错误原样带过来。如果你启动项目一堆依赖报错先检查 pom 里是不是混着老版本 starter再看javax和jakarta的包冲突。版本太高也不是好事。我踩过 Spring Boot 3.3 配某个低版本 Swagger 插件启动直接挂的坑后来统一用 springdoc-openapi 2.x 才消停。选版本的唯一标准是依赖生态匹配度不是“最新就是最好”。2. 数据模型和接口划分把一张产品表撑起整套门户2.1 核心表不贪多先把产品、资料、公告理清楚第一批表我设计得很细后来发现有点过度设计。真正上线稳定跑着的核心其实就几张表robot_product、document_resource、announcement、download_log。产品表不需要把每个机器人的几十个参数都拆成列。常用字段放表里剩余参数用 JSON 字段存前端展示灵活后台维护也不痛苦。robot_product的建表语句大概是这样CREATE TABLE robot_product ( id bigint NOT NULL AUTO_INCREMENT, model_name varchar(128) NOT NULL COMMENT 型号名称, series_code varchar(32) DEFAULT NULL COMMENT 系列编码, payload decimal(8,2) DEFAULT NULL COMMENT 额定负载(kg), reach decimal(8,2) DEFAULT NULL COMMENT 工作半径(mm), repeatability decimal(8,3) DEFAULT NULL COMMENT 重复定位精度(mm), cover_url varchar(255) DEFAULT NULL COMMENT 封面图, video_url varchar(255) DEFAULT NULL COMMENT 介绍视频地址, spec_url varchar(255) DEFAULT NULL COMMENT 规格书PDF地址, extra_params json DEFAULT NULL COMMENT 扩展参数, sort_order int DEFAULT 0 COMMENT 排序权重, status tinyint DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_series_status (series_code, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT机器人产品表;资料表的核心字段是product_id、doc_type、title、file_path、file_size、version、download_count。它不是简单的一张附件表因为同一份说明书可能会迭代好几个版本我加了version字段一个产品一个资料类型只展示最新版历史版本在后台保留。公告表就简单多了标题、内容、封面图、发布时间、置顶标记、状态。发布功能我建议拆成“保存草稿”和“发布”两种状态运营同学在后台编辑时不会因为手滑把半成品推给客户。2.2 接口路径和响应结构从根上避免前后端吵架所有接口统一带/api/v1前缀。加版本前缀不是为了装样子是为了后面移动端、小程序要复用同一套服务端接口的时候不用把所有路径重写一遍。实际接口清单按功能切别按表切GET /api/v1/products?page1size9keyword13kgseries_codeA产品分页GET /api/v1/products/{id}产品详情GET /api/v1/documents?product_id10doc_typemanual资料列表GET /api/v1/documents/{id}/preview文件预览GET /api/v1/documents/{id}/download文件下载POST /api/v1/admin/products后台新增产品PUT /api/v1/admin/products/{id}后台更新产品DELETE /api/v1/admin/products/{id}后台下架产品后端项目结构保持最简单的controller/service/mapper三层。内容门户用不上太多抽象过度分层只会让改需求时找文件的成本变高。响应结构统一用一个ResultT包装code0表示成功非 0 是业务错误码。数据校验错误、权限不足、文件不存在都要有明确的 code 和 message前端拿到之后弹对应提示不要让用户看到“系统异常”这种话。2.3 文件下载接口统计逻辑不能阻塞主流程下载功能是门户最容易忽略性能的地方。如果下载接口里先查库、再读文件、再 update 下载次数并发量一起来数据库连接会先被拖垮。我的做法是文件本身提前放磁盘目录接口里用ResponseEntityResource返回下载次数的累加丢到异步任务里。GetMapping(/{id}/download) public ResponseEntityResource download(PathVariable Long id) { DocumentResource doc documentService.getById(id); Resource file fileStorage.loadAsResource(doc.getFilePath()); documentService.increaseDownloadCountAsync(id); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ URLEncoder.encode(doc.getTitle(), StandardCharsets.UTF_8) \) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(file); }统计方法上标Async写在单独的组件里内部用独立事务更新下载数。这里有个大坑Spring Boot 默认通过 CGLIB 生成子类代理来实现Async但如果你在同一个类里this调用异步方法代理根本不会拦截方法还是同步执行。所以一定要把异步逻辑放到另一个 Bean 里或者直接注入ApplicationContext再取代理对象。3. Vue 前端落地门户信息架构和三类文件预览3.1 路由方案前台 history、后台 hash刷新不白屏前台产品页要有清晰 URL方便发链接所以用 history 模式形如/product/12。后台管理不需要 SEO直接用 hash 模式更省事这样部署到 Spring Boot 之后刷新/admin/products不会白屏。如果你非要在后台也用 history 模式那必须给 Spring Boot 加一个 fallback把所有不是/api的 GET 请求转到index.html否则 Spring Boot 会按静态资源目录去找/admin/products.html结果自然是 404。Controller public class SpaForwardController { GetMapping(value {/, /product/**, /docs/**, /news/**}) public String forward() { return forward:/index.html; } }产品详情页的参数用route.params.id读取列表页筛选用 query 传参?page1series_codeA。组件初始化时从route.query读条件切换筛选时用router.push更新 query这样页面刷新后地址栏参数还能保留筛选状态不会丢。后台权限这块如果角色只有管理员和运营两种不需要上动态路由。等权限复杂到“不同角色看到不同菜单”的时候再考虑登录后从接口拿菜单列表、用addRoute动态注册路由否则写死的路由表反而更安全。3.2 组件拆分的边界页面容器和展示组件分开首页拆成BannerHero、ProductShowcase、AnnouncementList、ContactFooter四块。产品展示组件不自己请求数据由父组件统一拉取产品列表后通过 props 传下来。这样后面做活动落地页或者其他入口想复用产品卡片时只需要改父组件的数据来源。组件拆分的原则很简单一个组件只回答一个问题。产品卡片只管展示型号、图片、负载和臂展这几个核心字段系列标签只管当前筛选状态空数据、加载中、接口异常三态一定要有独立展示别全用v-if塞在业务组件里。产品卡片大概长这样template div classproduct-card click$router.push(/product/${product.id}) img :srcproduct.coverUrl :altproduct.modelName / h3{{ product.modelName }}/h3 p负载 {{ product.payload }}kg / 臂展 {{ product.reach }}mm/p /div /template3.3 PDF、m3u8、压缩包文档中心绕不开的三类文件搜索引擎里能看到很多奇怪的问题vue image 能显示 PDF 吗答案是不能img只认图片格式你传一个 PDF 地址它只会显示破图。正确的预览姿势是iframe :srcpdfUrlpdfUrl 由后端提供需要页内缩放和标注时再上 vue-pdf-embed 或 pdf.js。这里有个后端响应头的问题后面上线时会单独说。只要后端把 PDF 的 Content-Type 返回成application/octet-stream浏览器就会把它当附件直接下载前端写得再好也没用。m3u8 是 HLS 流媒体格式浏览器原生不能直接播需要用 hls.js 去做解封装。判断方法很直接video.canPlayType(application/vnd.apple.mpegurl)返回空字符串时就用 hls.js 加载。import Hls from hls.js; function playM3u8(videoEl, src) { if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src src; } else if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(src); hls.attachMedia(videoEl); } }这样在 Chrome、Safari、Edge 里表现一致也不需要额外安装浏览器插件。压缩包下载直接给一个下载地址就能跑但如果是登录后才能下载的资料用window.open带 token 的方式容易暴露 token。我建议用 fetch 拉 blob再通过URL.createObjectURL触发下载文件特别大时用axios.onDownloadProgress做一个前端进度条体验好很多。4. 从开发到部署Vue 打包塞进 Spring Boot 的完整链路4.1 本地开发让 5173 指到 8080不用天天开 CORS本地开发时前端跑在 5173后端跑在 8080直接请求/api会跨域。Vite 本身提供了开发服务器的路径重写能力把/api开头的请求指到http://localhost:8080同时打开changeOrigin。这段配置通常写在vite.config.ts的server节点下后端不需要额外开 CORS。实际项目里我不建议在 Spring Boot 端全局放开跨域。开发方便归方便但一旦放开相当于给所有通过浏览器访问的站点发了一张访问许可生产环境没有任何好处。前后端同源部署之后跨域根本不存在。4.2 打包进 jar 的三种方式与资源路径陷阱把 Vue 放进 Spring Boot 这件事我试过三种办法本地构建前端把dist目录里的文件复制进src/main/resources/static再执行mvn package。方式最直观缺点是人肉操作容易漏。用frontend-maven-plugin在 Maven 打包时自动执行npm install和npm run build自动化程度高首次构建时间会比较感人。前后端彻底分离前端静态文件放独立的 Web 服务后端 jar 只管/api。适合迟早要把前端独立部署的团队。我最终用了第二种因为交付物只有一个可执行 jar现场部署不需要依赖 Node 环境。还有一个比接口连不通更隐蔽的坑如果后端设置了server.servlet.context-path/cobot那 Vite 打包时的base也要跟着改成/cobot/否则所有 JS/CSS 都从根目录找页面直接白屏。4.3 Spring Boot 版本差异、端口配置和 IDE 里的怪问题Spring Boot 3.x 和 2.x 最容易踩的坑是包名javax.servlet.*整体改成了jakarta.servlet.*。网上翻到 2.x 的过滤器、拦截器代码直接粘到 3.x 编译不过不要慌把 import 改掉大部分就能跑。端口配置也是新人高频问题。IDEA 里 Run/Debug Configurations 能看到三处可以改端口application.yml的server.port、Program arguments 里的--server.port8081、Environment variables 里的SERVER_PORT8081。优先级从高到低是环境变量、启动参数、配置文件。如果改了application.yml不生效十有八九是启动配置里残留了启动参数。我在 IDEA 2025 和 2026 的版本里都碰到过这种奇怪情况最后都是在 Edit Configurations 里找到旧的--server.port删掉的。另外不同环境建议用不同 profile 管理配置dev、prod端口分开不要靠记忆去改 yml。4.4 上线部署用 systemd 管住 java -jar部署到 Linux 服务器时不要把java -jar portal.jar挂在前台随便跑。我就吃过这个亏终端一关服务就没了。后来写了一个 systemd 服务[Unit] DescriptionPortal System Afternetwork.target [Service] Userportal WorkingDirectory/opt/portal ExecStart/usr/bin/java -jar /opt/portal/portal.jar --spring.profiles.activeprod Restarton-failure [Install] WantedBymulti-user.target上传目录不能放在 jar 内部。我在外层单独建了一个data/uploads目录Spring Boot 用配置项指向它这样升级 jar 时文件不会丢备份也只需要把这一个目录和数据库备份好。5. 上线前后实测坑位从资源响应头到缓存失效5.1 图片正常、PDF 却被浏览器当附件下载上线前测试时发现图片在商品卡片里显示正常点开规格书 PDF 却被浏览器直接下载了。用curl -I看了一下接口响应头发现 Content-Type 是application/octet-stream浏览器拿到这种类型只能当附件处理。后来我在文件预览接口里显式设置 Content-TypePDF 用application/pdf图片根据扩展名识别。Spring Boot 的静态资源映射/uploads/**一般能自动推断 MIME 类型但如果你走的是自定义接口返回文件流就得自己设置响应头。这就是为什么很多项目“vue image 能显示 PDF 吗”这类问题的背后前端其实没毛病后端响应头先错了。前端只是背锅侠。5.2 下载统计一夜暴涨问题出在 HLS 视频分片HLS 播放时浏览器会连续请求很多 ts 分片。前端如果直接把视频地址填成/documents/{id}/download后端会把每一片都当成一次资料下载统计一夜之间多几十万是轻轻松松的事。我的处理方式是把视频播放单独拆成/documents/{id}/stream接口stream 接口只输出流、不累加统计真正点击“下载”按钮才走 download 接口。download 接口还要做防刷同一个登录用户一分钟内超过 30 次就返回 429同时记录 userAgent 和 IP 用于后续分析。门户系统的下载量是要拿给销售和市场看的数据不准比没有还麻烦。5.3 后台改完前台不更新缓存版本号救场产品参数和公告列表做成缓存之后第一次上线就翻车了运营在后台把产品文案改了前台页面还是旧数据。排查后发现问题不在接口数据库里的update_time确实更新了Redis 缓存里还是老值。后来我把缓存 key 设计成带版本号portal:products:page:1:size:9:v${version}后台每次保存产品时把 version 自增前端读缓存时发现 key 变化自然回源。简单粗暴但非常可靠比一个个删 key 靠谱多了。如果后面要把版本发布通知推到内部群或者接 ActiveMQ 做异步通知也是顺着这套异步处理的思路往后加不会影响主流程。最后再提醒一句维护期的事情门户跑起来之后真正让人头大的往往不是接口而是运营同学慢慢改出来的各种内容格式问题。富文本粘贴过来的样式会污染全局一定记得给渲染区域加 CSS 隔离下载文件命名要统一成“型号-资料类型-版本号”否则客户下载后全是一堆 document.pdf。这些细节比写 CRUD 更影响客户对整套系统的口碑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑