
上个月刚把一个新疆巴州维药推广平台的前后端代码从零整理完趁着热乎劲儿把过程写下来。这个项目用的是 Java Spring Boot 做后端、Vue 做前端不是一个挂着“毕设”头衔的空壳 Demo而是真正能跑、能上线、能继续迭代的整套系统。这个平台的关键词其实不是“药品展示”而是“推广”。巴州的维药资源分散在不同医疗机构和药店里很多药材、方剂、传统民俗验方只停留在纸质资料和口口相传中普通用户根本搜不到。所以我要做的不是一个卖药的商城而是一个把维药知识、方剂信息、专家资源、健康资讯集中起来的内容服务平台。如果你最近在找 Java 全栈项目练手或者正打算做类似的信息类平台这篇记录应该能给你一个比较完整的参考。我从需求拆解、数据库设计、后端接口、Vue 前端到部署上线的坑都会讲清楚为什么这么做而不是只贴代码。1. 这个项目不是在写展示页而是把需求拆成了三层业务1.1 推广平台的真实业务范围很多人一看到“维药推广平台”第一反应是做一个药品分类列表点进去看详情完事。但真去梳理需求就会发现单纯展示页只解决了“能看到”的问题解决不了“怎么更新、怎么管理、怎么让内容可信”的问题。我最后落地的版本实际上是一个“内容生产 信息展示 后台运营”的三层结构内容生产层管理员、内容编辑录入药品、方剂、资讯内容要经过审核才能发布信息展示层游客和注册用户浏览维药百科、方剂库、专家介绍、健康资讯可以收藏、留言后台运营层管理药品分类、审核内容、管理用户、查看留言、配置首页轮播图。这样的设计让平台不是一个静态网站而是可持续运营的内容资产。对开发来说它的难点也不在前端样式而在数据模型、权限控制和内容状态管理。1.2 六类用户角色到底谁在用什么功能我把用户角色分成六类分别对应不同的功能边界角色核心诉求能做什么游客快速了解维药浏览百科、方剂、资讯搜索药品注册用户收藏和参与互动登录后收藏药品、提交咨询留言内容编辑录入药品和资料维护百科、方剂、资讯提交审核审核员保证内容专业可信审核编辑提交的内容可驳回管理员平台整体运营用户管理、角色分配、栏目配置、数据统计专家/顾问提供专业背书维护个人主页回复咨询留言为什么把“内容编辑”和“审核员”拆开因为内容质量是这个平台的命根子。如果随便一个账号就能直接发布药品信息平台很容易变成“信息垃圾场”。我宁可让流程多一步审核也不能让错误内容直接暴露给用户。1.3 我列功能清单时的取舍标准功能清单不是越多越好我给自己定了一个标准凡是不能让平台“更好推广维药”的功能先砍掉或者往后放。比如在线交易我一开始就没做。药品交易涉及资质、库存、物流、售后复杂度翻倍而且对内容型平台来说并不是核心。又比如在线视频问诊技术可行但需要对接第三方音视频服务成本不低需求也没那么明确我就做成了“留言咨询 专家回复”的异步模式。最后保留的功能模块是门户首页、维药百科、方剂库、专家团队、健康资讯、咨询留言、用户中心、后台管理。每个模块都能说清楚“服务谁、解决什么问题”。如果你也在做类似的毕设或实际项目建议先按这个思路把功能清单写出来而不是打开数据库就开始建表。2. 技术栈为什么会落在 Spring Boot 3 Vue 3 这套组合上2.1 后端不选 PHP 也不选 Node选 Spring Boot 的原因项目标题既然写了 Java Spring Boot这个方向本身就有很强的现实原因国内大部分信息管理系统、后台管理系统Java 生态的交付资料最完整招人也好招。但具体到版本选择我建议新项目直接上 Spring Boot 3.x配合 JDK 17。Spring Boot 3 相比 2.x 最大的变化是底层基于 Spring Framework 6默认使用 Jakarta EE 命名空间启动更快对 GraalVM 原生镜像的支持也更好。虽然项目里没有用到云原生特性但起点高一点后续扩展不费劲。我选 Spring Boot 的核心原因有三个Starter 依赖把配置量压得很低写一个 Web 服务只需要spring-boot-starter-web内嵌 Tomcat不需要单独部署容器和 Spring Security、Spring Data JPA、MyBatis-Plus 等生态结合顺手权限、持久化这些“脏活”都有成熟方案项目部署很简单打成一个 jar 包就能跑配合 systemd 或 Docker 都比较方便。2.2 前端选 Vue 3 而不是 React 的现实理由前端选 Vue 3不是说 React 不好而是这套组合在这个项目里性价比最高。维药推广平台的前端由两块组成对外展示的信息门户 内部使用的后台管理界面。Vue 3 的 Composition API 让逻辑复用方便了很多配合 Vite 开发服务器的热更新速度体感比 Vue 2 webpack 时代快非常多。Vue 3 生态里还有几个项目直接依赖的组件Element Plus 做后台管理界面表格、表单、弹窗、树形控件都有现成的Pinia 做全局状态管理比 Vuex 的样板代码少Vue Router 做路由支持路由懒加载和导航守卫Axios 做 HTTP 请求拦截器处理 token 和统一错误提示。中文文档完整社区案例多遇到问题搜一下基本都有答案。对项目开发来说能快速找到解决方案本身就是最大的效率。2.3 前后端分离之后开发流程反而更顺畅开发时我会启动两个服务后端跑 8080 端口前端跑 Vite 的 5173 端口。Vite 配置代理把/api开头的请求转发到后端。这样前端写页面时不需要关心跨域后端调试接口时也不需要前端打包。生产环境则反过来前端npm run build生成静态文件放到 NginxNginx 把/api请求反向代理到后端的 jar 服务。前后端分离的好处是前端可以独立部署到 CDN后端可以横向扩展以后就算要给平台加一个小程序端接口还是这套。3. 数据库设计把维药、方剂、资讯、会员串成一张业务网3.1 维药基础表先把“药学属性”和“运营属性”分开数据库是整个平台最需要花时间的部分。我设计drug表时刻意把两类字段分开一类是药学专业属性一类是平台运营属性。药学属性包括药品名称、别名、拼音码、药材来源、四气、五味、归经、功效、主治、用法用量、禁忌。运营属性包括分类、封面图、摘要、状态、浏览量、排序、创建人、审核状态。为什么要分开因为药学属性是相对固定的知识数据运营属性是平台运作产生的业务数据。如果混在一起列表页展示摘要和状态时还要连带查询大段功效文本性能是一方面逻辑上也容易乱。药品表的核心字段我大致设计成这样CREATE TABLE drug ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称, alias_name VARCHAR(255) DEFAULT NULL COMMENT 别名, category_id BIGINT DEFAULT NULL COMMENT 分类ID, pinyin_code VARCHAR(50) DEFAULT NULL COMMENT 拼音码用于搜索, source_place VARCHAR(255) DEFAULT NULL COMMENT 药材来源, nature VARCHAR(50) DEFAULT NULL COMMENT 四气寒热温凉平, taste VARCHAR(100) DEFAULT NULL COMMENT 五味酸苦甘辛咸, meridian VARCHAR(100) DEFAULT NULL COMMENT 归经, efficacy TEXT COMMENT 功效, indications TEXT COMMENT 主治, usage_dosage TEXT COMMENT 用法用量, contraindication TEXT COMMENT 禁忌, summary VARCHAR(500) DEFAULT NULL COMMENT 摘要, image_url VARCHAR(500) DEFAULT NULL COMMENT 药品图片, status TINYINT DEFAULT 0 COMMENT 状态0草稿 1待审 2通过 3驳回, views INT DEFAULT 0 COMMENT 浏览量, sort INT DEFAULT 0 COMMENT 排序, create_by BIGINT DEFAULT NULL COMMENT 创建人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维药药品表;这里有个细节搜索字段我用的是pinyin_code不是直接对drug_name做模糊查询。维药名称有汉语、可能有维吾尔语转写用户常记不准完整名字存一个拼音码能让搜索更友好。这个字段可以由编辑录入时同步生成也可以用工具批量生成。3.2 方剂与药物的多对多关系用明细表承载方剂是维药推广里很有特色的内容一个方剂通常由多味药组成一味药也可能出现在多个方剂中。这种关系不能简单地在药物表里加一个prescription_id标准做法是中间明细表。CREATE TABLE prescription ( id BIGINT NOT NULL AUTO_INCREMENT, prescription_name VARCHAR(255) NOT NULL COMMENT 方剂名称, source VARCHAR(255) DEFAULT NULL COMMENT 来源, efficacy TEXT COMMENT 功效, indications TEXT COMMENT 主治, usage_method TEXT COMMENT 用法, caution TEXT COMMENT 注意事项, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT方剂表; CREATE TABLE prescription_drug ( id BIGINT NOT NULL AUTO_INCREMENT, prescription_id BIGINT NOT NULL COMMENT 方剂ID, drug_id BIGINT NOT NULL COMMENT 药物ID, dosage VARCHAR(100) DEFAULT NULL COMMENT 方剂中的用量, sort INT DEFAULT 0 COMMENT 排序, PRIMARY KEY (id), KEY idx_prescription (prescription_id), KEY idx_drug (drug_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT方剂药物明细表;有了这张明细表很多需求就能用 SQL 直接回答查某个方剂的完整组成按prescription_id关联drug查某味药出现在哪些方剂里按drug_id反查prescription。如果用 JSON 字段存组成展示是方便了后续统计和筛选会非常痛苦。3.3 内容发布、用户收藏和咨询留言的表结构设计药品和方剂之外还要考虑资讯文章、用户、收藏、咨询留言这几类数据。资讯表参考普通 CMS 的设计字段包括标题、封面图、摘要、正文、栏目、标签、状态、发布时间。用户表则需要存用户名、密码、昵称、头像、手机号、状态以及和角色表的多对多关系。收藏表我设计得比较通用内容是药品还是方剂还是文章用一个biz_type字段区分再存biz_id。这样一张表就能支持“我收藏的药品”“我收藏的方剂”“我收藏的资讯”三个页面不需要为每种业务建一张收藏表。咨询留言表相对简单用户ID、内容、联系方式、状态、管理员回复内容、回复时间。注意不要把咨询设计成即时聊天否则还需要引入 WebSocket 或第三方客服系统对一个推广平台来说太重了。3.4 一个容易被忽略的字段审核状态做信息类平台最容易忽略的就是内容的status字段。从编辑录入到前端展示中间至少要有“草稿、待审、通过、驳回”几个状态。前端所有查询都只查status 2通过的数据编辑只能看到自己创建的草稿审核员能看到所有待审记录。这个状态字段贯穿药品、方剂、资讯三张表是我在代码里反复强调的一条铁律列表接口必须带状态条件否则就会发生“刚录入的草稿直接出现在门户首页”的事故。4. 后端接口与权限不要每个接口都自己写登录判断4.1 JWT Spring Security 怎么组织权限权限是后台管理系统的重点。这个平台包含公开接口和后台接口最简单的实现方式是 JWT 登录态 Spring Security 过滤器链。用户在登录接口输入用户名密码校验通过后生成一个 JWT返回给前端。前端每次请求在Authorization头里带上Bearer token后端过滤器解析 token把用户信息和角色放进 SecurityContext后续接口就能用注解控制权限。核心配置大致是这样http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/drugs/**, /api/articles/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);公开接口放行后台接口必须要有管理员角色。这里不建议自己写一个拦截器在每个 Controller 里判断 sessionSpring Security 的过滤器链在“请求进入业务方法之前”统一处理代码干净很多。4.2 药品分页检索接口条件多但代码要干净药品列表页会支持关键词搜索、分类筛选、分页展示。对应的后端接口可以接收keyword、categoryId、page、size几个参数。用 MyBatis-Plus 的 LambdaQueryWrapper 写起来比较直观LambdaQueryWrapperDrug wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(query.getKeyword())) { wrapper.and(w - w.like(Drug::getDrugName, query.getKeyword()) .or().like(Drug::getAliasName, query.getKeyword()) .or().like(Drug::getPinyinCode, query.getKeyword())); } if (query.getCategoryId() ! null) { wrapper.eq(Drug::getCategoryId, query.getCategoryId()); } wrapper.eq(Drug::getStatus, 2) .orderByAsc(Drug::getSort) .orderByDesc(Drug::getViews);注意关键词搜索里的三个字段要用and(...)包起来否则分类筛选和关键词筛选会互相干扰。这个细节如果你直接写like拼接很容易拼错 SQL 逻辑。4.3 药品详情页需要聚合哪些数据药品详情页不只是展示drug表的一行数据它还需要展示分类名称、相关方剂列表、录入人信息。所以后端不能直接返回实体对象而是返回一个聚合 VO。我的实现思路是第一步根据 ID 查询药品信息如果状态不是“通过”且当前用户不是管理员直接返回不存在第二步根据drug_id查询prescription_drug关联表再关联prescription表拿到方剂名称、功效第三步把药品浏览量加一异步执行避免影响详情响应速度。为什么这里不用一次大 SQL 全部 join 出来因为药品详情和方剂列表是两个独立展示区域分开查反而更容易控制缓存。以后如果方剂列表要做分页只要改第二步就行。4.4 内容审核状态与管理员操作闭环内容编辑提交后状态从“草稿”变成“待审”。审核员在后台看到待审列表点击通过或驳回。如果驳回必须填写原因这个原因会通过消息或列表提示反馈给编辑。这个过程我在后端用一个统一的audit接口处理参数包含业务类型、业务 ID、审核结果、审核意见。管理员不需要分别写“审核药品”“审核方剂”“审核文章”三个接口而是通过bizType分发到不同的 Service 方法。这个设计让后续扩展新内容类型时不用重复造轮子。比如以后要加“视频科普”只需要在内容类型字典里加一个枚举审核逻辑自动生效。5. Vue 前端从路由到页面的落地细节5.1 路由懒加载和角色守卫前端最怕的是把所有页面一次性打包首屏加载慢路由还乱。我的做法是每个一级页面都用动态导入让 Vite 自动分包。核心路由大概长这样const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /drugs, name: DrugList, component: () import(/views/DrugList.vue) }, { path: /drug/:id, name: DrugDetail, component: () import(/views/DrugDetail.vue), props: true }, { path: /login, name: Login, component: () import(/views/Login.vue) }, { path: /admin, component: () import(/layouts/AdminLayout.vue), meta: { requiresAuth: true, roles: [ADMIN] }, children: [ { path: , redirect: /admin/dashboard }, { path: drugs, component: () import(/views/admin/DrugManage.vue) } ] } ]路由守卫里做两件事一是判断requiresAuth没有 token 就跳登录页二是判断角色不是管理员访问/admin就提示无权访问。注意不要把角色判断逻辑散落在每个页面里统一放在beforeEach里维护最省心。5.2 Axios 封装与登录态过期处理前端所有请求都走一个封装好的 Axios 实例统一配置baseURL和超时时间。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token useUserStore().token if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) } return Promise.reject(error) } )这里有一个容易踩的坑后端返回的数据格式要统一。我定的是{ code: 0, msg: success, data: ... }前端在响应拦截器里直接返回response.data页面代码就不用每处都写res.data.data。如果格式不统一整个项目的取数逻辑会非常混乱。5.3 维药列表页和详情页的组件划分维药列表页看起来简单但写起来很容易膨胀。我的做法是把页面拆成几个组件CategoryFilter负责分类筛选接收分类列表和选中值DrugCard负责单条药品卡片展示包括图片、名称、功效摘要Pagination封装 Element Plus 分页组件统一处理页码变化useDrugList一个组合式函数内部维护列表数据、加载状态、查询参数。把请求逻辑抽到组合式函数里之后页面组件只负责模板和事件绑定代码量少了一半。这个思路对后台管理页面同样适用搜索条件、表格数据、分页这三件事本来就是固定搭配。5.4 后台管理界面别重复造轮子后台管理我直接用 Element Plus 的el-table、el-dialog、el-form组合。药品管理、方剂管理、资讯管理、用户管理四个页面结构高度相似如果每个页面都复制粘贴一遍后续改起来会想哭。我抽了一个CrudTable组件把“搜索表单 表格 分页 新增弹窗 编辑弹窗 删除确认”的结构封装起来通过插槽和属性传入定制内容。当然这个组件不能做得太死否则灵活性不够。折中方案是先把药品管理和资讯管理两个页面做出来找到相似点再抽组件不要一开始就抽象。6. 部署上线与生产环境的真实考验6.1 Nginx Spring Boot Jar 的经典部署方案项目部署是最能照出问题的地方。前端先执行npm run build产出dist目录把它放到服务器的/app/web目录。后端执行mvn clean package -DskipTests产出 jar 包放到/app/backend。Nginx 配置要注意两点静态资源根目录指向dist接口反向代理到 8080。server { listen 80; server_name your-domain.com; root /app/web; 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; } location / { try_files $uri $uri/ /index.html; } }最后一行try_files $uri $uri/ /index.html;必须写。因为 Vue Router 用的是 history 模式用户直接访问/drug/123时Nginx 找不到这个真实路径如果不 fallback 到index.html刷新页面就是 404。6.2 图片文件到底放在哪里本地目录还是 MinIO药品图片、资讯封面、用户头像都是文件上传需求。很多新手会直接把文件写到项目的static/images目录这在本地跑没问题但生产环境会遇到两个麻烦重新部署时文件可能丢多台服务器时文件不同步。我的建议是分阶段处理开发演示阶段上传到服务器本地/data/uploadsNginx 里加一个location /uploads/映射即可正式上线或以后要扩容接入 MinIO 做对象存储Java 端用minio客户端 SDK 上传保存返回的 URL。MinIO 的好处是部署简单一个 Docker 容器就能起一个兼容 S3 协议的存储服务。接入逻辑也不复杂核心就是把上传文件从“写本地磁盘”换成“写对象存储桶”前端拿到的图片地址不变。6.3 跨域、时区、编码这三个不起眼的坑开发时前端用 Vite 代理生产用 Nginx 同域代理所以跨域问题基本被绕开了。但如果你在本地前后端分离联调时直接让前端访问localhost:8080就会遇到跨域。临时处理可以配置 CORS但我不建议在每个 Controller 上加CrossOrigin。更好的方式是在后端加一个全局过滤器开发环境放开生产环境关闭。因为生产环境走 Nginx 同域根本不需要 CORS反而会带来安全隐患。时区问题更容易忽略。MySQL 连接串里一定要加serverTimezoneAsia/Shanghai否则时间字段可能差 8 小时。编码问题也一起说数据库连接串加characterEncodingutf8建表用utf8mb4前端接口返回的内容自然就是正常的。这三个坑不解决表现出的现象看起来很随机但根因都很直白。6.4 内容更新、数据库备份与日常运营建议平台上线后真正的挑战是内容持续更新。药品和方剂资料不是录入一次就完了可能有修订、纠错、补充图片等需求。审核状态机在这里会持续发挥作用编辑改完旧内容后内容应该重新进入“待审”而不是直接覆盖线上内容。数据库备份必须提前做。最简单的方式是每天凌晨用mysqldump备份一次保留最近 7 天。如果数据库跑在 Docker 容器里就用docker exec在容器内执行导出命令然后复制到宿主机。数据是平台最值钱的东西代码可以重新写数据丢了没法重建。7. 复盘如果重新做一次这几个地方我会先改这个项目做完之后我回头看最想调整的不是某个页面样式而是几个基础设计。第一个是数据模型要加字典表。药品的“四气五味”目前用的是字符串字段如果只是展示没问题但以后想按“寒性药材”“温性药材”做筛选直接在字符串里模糊匹配效率低也容易错。正确做法是维护一套字典字段里存字典值筛选用字典 ID。第二个是权限模型应该加“数据范围”。现在管理员能看到所有内容内容编辑只能看自己的这个用create_by过滤实现了。但如果以后有多个科室、多个内容栏目还要支持“这个编辑只能管某个栏目”的细粒度权限。一开始就把用户和内容栏目的关系表建出来后面会省很多事。第三个是引入 Flyway 做数据库版本管理。项目开发期经常有人改表结构每人本地一套 SQL合并后容易漏执行。Flyway 能保证数据库脚本按版本顺序执行团队协作时不会出现“我本地能跑你本地报错”的情况。第四个是前端的 API 请求函数要集中管理。现在页面里直接调用封装好的 Axios 实例虽然拦截器统一了但每个接口的 URL 和参数散落在页面中。如果抽成api/drug.js、api/article.js这样的模块后面维护会舒服很多。这类平台的核心从来不是代码炫技而是把内容生产、审核、展示、反馈这条链路做顺。技术选型只是保障真正花时间的往往是字段命名、状态设计和运营规则。如果你也想做类似系统先把这些想清楚后面能少踩一大半的坑。