
每年到这个时候后台收到最多的私信就是“博主毕业设计选什么题目好”。技术栈绕来绕去最后还是绕回Java生态里最稳的那个组合——springboot。今天我想认真聊聊“网上购药系统”这个题目它不是普通的电商CRUD药品这个品类决定了它有很多普通商城没见过的业务细节。这篇文章会把我的设计思路、表结构、核心接口实现、以及连答辩都会被追问的坑全部拆开讲适合正在做选题还没确定方向、或者题目已经定了但不知道怎么落地的同学参考。先说一个基本判断这个选题最大的优势是“业务足够日常技术足够通用”。评委不需要你解释药品是什么也不需要你普及电商是什么大家都有生活经验。但对开发者来说它比普通图书商城多了一层药品特有约束——处方药和非处方药怎么区分、库存批次有效期怎么处理、购物车到下单的事务边界在哪。这些约束恰恰是论文里“需求分析”和“系统设计”章节最好写的素材也是答辩时最能体现你思考深度的位置。1. 项目整体设计与思路拆解1.1 为什么选springboot 网上购药这个组合网上购药系统的本质是电商垂直领域应用但它的信息管理复杂度明显高于普通商品系统。我见过太多毕业生把药品信息做成一张简单的商品表name、price、stock三段式糊弄过去结果论文评审一眼就看穿——没有批号、没有生产日期、没有有效期、没有处方药标记这根本没办法落地。springboot在这个场景下的优势非常明确。第一它把项目从配置泥潭里解放出来内置Tomcat、自动配置、起步依赖这套机制让开发重心完全转移到业务逻辑上两周时间足够把前台购物和后台管理都跑通。第二Java生态对电商类项目的支撑极其成熟MyBatis-Plus、Redis、定时任务、JWT这些毕设常用组件都有大量现成案例遇到问题搜索一下就有解。第三springboot项目结构规范、分层清晰controller、service、mapper、entity这种结构本身就是论文里“系统设计”章节的最佳素材评委看着舒服你写起来也顺手。另外要泼一盆冷水不要一上来就想着微服务、分布式、消息队列、ElasticSearch。毕设的核心评价标准是“完整、逻辑自洽、有细节深度”把单体应用做好、把事务和权限想清楚已经能拿高分。盲目堆技术只会给自己挖坑答辩时一个“为什么不用XXX”就能把你问住。1.2 业务模块划分从普通商城到药房系统网上购药系统如果只做商品展示、购物车、下单、支付那它和图书商城没有任何区别论文毫无亮点。药品电商的差异化业务点主要有三个。第一个是处方药流程。处方药不能直接下单购买需要用户上传处方单图片系统记录审核状态由后台管理员审核通过后才能发货。这个状态机的设计是实体药店管理系统和普通电商最大的区别也是你系统中“订单状态”不能只做“待付款、已付款、已发货、已完成”这四个状态的原因。完整状态应该拆成待上传处方、处方审核中、处方未通过可重新上传、待付款、已付款、已发货、已完成、已取消。这个细节一定得写进需求分析。第二个是药品信息的字段结构。除了通用的标题、图片、价格、描述、分类外药品还需要批准文号国药准字、生产厂家、剂型片剂/胶囊/口服液等、处方类型RX处方药/OTC非处方药、库存批次信息生产批号、生产日期、有效期至。有效期这个字段很关键因为药品过期绝对不能售卖所以列表查询时要自动过滤有效期已过的批次后台也要有临期预警这就自然引出了定时任务的需求。第三个是购药场景下的用户辅助功能。比如用药提醒、在线问药、药品说明书查看。这些模块不必做得重但存在本身就展示了你的需求分析能力——你知道药品不是普通商品用户需要说明书、用法用量、禁忌信息。我建议至少做“药品详情页展示完整说明书”和“个人中心的用药提醒列表”这两个轻量功能实现成本低但论文价值不小。1.3 数据库设计八张表的关联关系表结构设计是整个系统的地基我直接给出经过验证的落地方案共八张表。用户表userid、username、passwordBCrypt加密存储、real_name、phone、id_card实名购药需要、roleUSER/ADMIN、status、create_time。这里强调一下role字段用字符串或枚举值都行但一定不要单独设计权限表毕设规模只需要在拦截器里判断角色即可。药品表drugid、category_id、name、generic_name通用名、approval_number批准文号、manufacturer、dosage_form剂型、prescription_typeRX/OTC、price、image、description、stock总库存、sales销量、status上架/下架、create_time。注意销售量和库存是两个概念销量用于热销排序和后台统计。药品批次表drug_batchid、drug_id、batch_number生产批号、production_date、expire_date、quantity、remaining_quantity。为什么单独拆表因为同一种药品会多次进货每批有效期不同。库存扣减时优先扣“先过期”的批次这就是药品ERP里常见的FEFOFirst Expired First Out原则论文里写出来是加分项。分类表categoryid、name、parent_id、sort。做两级分类就够了比如“感冒用药”下面再分“解热镇痛”“止咳化痰”。购物车表cartid、user_id、drug_id、quantity、create_time。联合唯一索引user_id, drug_id防止同一药品加两次。订单表ordersid、order_no唯一业务单号、user_id、total_amount、status、address_snapshot、prescription_status、create_time、pay_time、deliver_time、finish_time。地址快照这个细节要注意下单时把收货地址的文本快照直接存进订单不要用外键关联地址表否则用户之后改地址会影响历史订单展示。订单明细表order_itemid、order_id、drug_id、drug_name、price、quantity、prescription_type。冗余drug_name和price是因为药品信息可能变更而订单明细必须保留历史快照。收货地址表addressid、user_id、receiver、phone、province、city、district、detail、is_default。八张表的关系很清晰用户—购物车—药品多对多通过cart关联用户—订单—订单明细一对多药品—药品批次一对多。这套结构不复杂但每个设计决策地址快照、批次库存、状态冗余都能在答辩时讲出道理。2. 核心技术选型版本与数据访问层的坑2.1 springboot版本怎么选2.7.x还是3.x这个问题最近几乎每三天就被问一次因为SpringBoot 3.0在2022年底发布后网上教程开始大量分裂成两个方向。我的建议非常直接毕设用springboot 2.7.x系列不要用3.x。原因有三点。第一3.x基于Jakarta EE 9命名空间javax变成了jakarta很多老教程和代码片段直接复制会报包不存在。你从网上找的每个demo都可能是旧命名空间到时候排查问题的时间完全超出想象。第二3.x要求JDK 17而大部分学校的课程和环境还停留在JDK 8如果你电脑上既有课程设计要求的旧项目又有毕设来回切换环境就是噩梦。第三Spring Security、MyBatis-Plus、shiro等生态组件对SpringBoot 3的支持虽然已经跟上但很多低版本组件不兼容你搜到的解决方案和你实际报的错经常对不上。选版本时顺便把配套的JDK、MyBatis-Plus版本一起锁定我建议这套稳定组合JDK 8 SpringBoot 2.7.18 MyBatis-Plus 3.5.3 MySQL 8.0 Redis 5.x。这套组合经过大量毕设项目验证资料最全、坑最少。注意如果你的学校明确要求必须使用JDK17以上环境那才考虑SpringBoot 3.2.x同时确认你依赖的每个组件都支持Jakarta命名空间。不要因为教程新就选3.x稳定压倒一切。2.2 数据访问层MyBatis-Plus的正确打开方式网上购药系统的数据访问层用MyBatis-Plus是性价比最高的选择。它同时提供了BaseMapper的通用CRUD方法和自定义SQL的能力毕设期间的开发速度能提升一倍。pom.xml里核心依赖片段如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency一个关键配置MyBatis-Plus的逻辑删除和分页插件。逻辑删除就是在表中加deleted字段删除操作变成update这是电商系统的惯例防止误删导致损失。分页插件配置一个Configuration类即可Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里要提醒一个容易踩的坑逻辑删除字段如果配置了但查询时忘了加TableLogic注解删除操作会变成物理删除数据就真没了。正确做法是在实体类deleted字段上加TableLogic并且在全局配置里指定逻辑未删除值0、已删除值1。复杂查询用自定义SQL。比如药品列表页需要连分类表查分类名称需要按销量排序还需要按药品名称模糊搜索这种多表联查不要硬用BaseMapper直接在Mapper接口里写Select注解更清晰Select(SELECT d.*, c.name AS category_name FROM drug d LEFT JOIN category c ON d.category_id c.id WHERE d.deleted 0 AND d.status 1 AND (d.name LIKE CONCAT(%, #{keyword}, %) OR d.generic_name LIKE CONCAT(%, #{keyword}, %)) ORDER BY d.sales DESC) IPageDrugVO selectDrugPage(Page? page, Param(keyword) String keyword);2.3 缓存、定时任务与配置细节缓存这里我建议用Redis做两块热销药品列表缓存和购物车数量缓存。不要过度设计刚毕业的项目用Redis缓存所有药品详情反而容易造成缓存一致性问题到时候改价改不显示答辩现场就尴尬了。网上购药系统的定时任务是真正的业务刚需这是区别于普通商城的又一个亮点。用springboot自带的Scheduled就够不需要引入Quartz除非你要做分布式定时任务。典型需求有三个凌晨自动把过期批次的药品标记为不可售update drug_batch set status0 where expire_date curdate()每天检测临期批次有效期30天内生成预警列表模拟订单超时未支付自动取消。第3个尤其重要——用户提交订单后15分钟未付款系统应该自动将订单置为已取消并回补库存。很多同学忽略了这个功能导致模拟支付时总有些卡单数据在线演示就会露怯。一个容易被忽略的配置问题是事务。springboot默认事务管理器已经配置好关键是要记得在service方法上加Transactional。BUT要注意如果同一个类内部调用另一个方法事务注解会失效因为Spring事务基于AOP代理this调用不走代理。下单流程里“扣库存创建订单创建订单明细”必须放在同一个事务方法里而且要跨Service调用才能保证代理生效。3. 核心模块实操从登录到下单的完整闭环3.1 用户登录与鉴权方案毕设级别的登录鉴权我推荐JWT而不是Spring Security因为Spring Security的过滤器链对新手来说是黑盒配置出错后报错信息极其不友好。JWT需要清晰的思路用户登录成功后生成token返回给前端前端每次请求在Header里带Authorization字段后端写一个拦截器解析token并放入用户上下文。核心类是TokenUtil和AuthInterceptor。TokenUtil负责生成和解析tokenJWT密钥、过期时间写在application.yml里jwt: secret: your-own-secret-key-please-change-me expire: 86400000拦截器注册Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }还有一个容易被追问的点管理员和普通用户的权限区分。推荐做法是拦截器校验完登录后再判断一次角色。比如后台管理接口统一走/admin/路径拦截器里判断role字段是否ADMIN不是就返回403。这个方案简单可靠比注解式权限控制更适合在答辩时讲清楚。前端用Vue3 Element Plus的话axios请求拦截里统一从localStorage取token即可服务端401时统一跳转登录页。3.2 药品浏览与搜索模块药品列表页是前台核心页面要求支持分类筛选、关键词搜索、销量排序、价格排序、分页展示。还有一个细节很容易被忽略处方药在列表页普通用户可见但不能直接加购物车点击购买时弹窗提示“处方药需先上传处方并由药师审核”。这个逻辑的代码并不复杂但它是整个系统“懂业务”的体现。药品详情页要完整展示说明书字段批准文号、性状、功能主治、用法用量、不良反应、禁忌、贮藏条件。建议在数据库表里加一个说明书字段或者用一个JSON字段存储否则详情页太单薄。考虑答辩演示时最好在主图下方加入一个高亮标签“RX处方药”或“OTC非处方药”视觉效果更直观。搜索功能不建议用ElasticSearch直接MySQL的LIKE模糊查询足够应付毕设几千条药品数据。真要体现优化思路可以讲一下前端防抖和后端的SQL索引设计——在drug表的name、generic_name字段上加联合索引这是性价比最高的方案。3.3 购物车与订单模块并发场景下别踩的坑购物车接口比较常规加入购物车存在就数量1、修改数量、删除、单选/多选结算。这个部分重点在后端校验加购数量不能超过库存总数、结算时必须是选中状态、购物车中已下架的药品要自动置灰并在结算时排除。订单模块是整个系统的核心也是最容易被问出问题的地方。下单接口的并发场景一定要有意识两个用户同时买最后一件商品怎么办数据库层面的扣库存SQL要带上库存条件确保不会超卖UPDATE drug SET stock stock - #{quantity} WHERE id #{drugId} AND stock #{quantity}受影响行数为0说明库存不足事务回滚。这是乐观锁思路的经典用法。下单流程完整如下Transactional public Order createOrder(OrderCreateDTO dto, Long userId) { // 1. 校验购物车选中项、检查药品状态和库存 // 2. 扣减库存注意按批次FEFO顺序扣减 // 3. 生成订单主表记录状态为待付款或待上传处方 // 4. 生成订单明细列表 // 5. 清空对应购物车项 // 6. 返回订单对象 }订单状态机前面已经列过这里补充一个实现细节状态流转要写成一个独立方法比如cancelOrder、payOrder、reviewPrescription、deliverOrder不要在每个Controller里散着写update语句。这个设计能让状态变更逻辑集中在Service层答辩时能清晰讲出状态机设计。关于模拟支付强烈建议不要接第三方真实支付接口资质和审核流程你走不通而是做一个模拟支付页面输入任意内容点击确认支付后端把订单状态置为已付款并记录支付时间。在论文里明确写“为演示方便支付模块采用模拟实现”完全没有问题这是毕设惯例。3.4 后台管理模块把CRUD做出管理系统的样子后台管理分四大块药品管理、分类管理、订单管理、处方审核。药品管理除了常规的增删改查外要有批量上下架功能和库存批次管理界面——毕竟批次表是和普通商城最大的区别之一。处方审核是这个系统的灵魂模块管理员能看到待审核的订单点击查看用户上传的处方图片选择通过或不通过。审核不通过要填写原因并且系统要自动给用户发送站内消息通知。后台界面用Vue3 Element Plus的话表格组件自带分页、排序、筛选处理起来很快。几个具体功能的权限控制不要忘了药品管理接口、订单管理接口、处方审核接口必须加管理员角色校验普通用户访问直接403。前端路由也要对应配置动态权限不要让普通用户看到管理菜单。4. 测试、部署与问题排查实录4.1 本地开发与接口调试经验前后端分离项目本地联调时面临的最大问题是跨域。建议在springboot侧配置CORS全局跨域策略而不是在前端搞代理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }注意allowCredentials(true)和allowedOriginPatterns()的组合写法如果allowedOrigins写会因跨域凭据规则报错这是新手最常踩的坑之一。接口调试过程中我建议一定要用Postman或Apifox把每个接口的异常场景也测一遍未登录访问、库存不足下单、处方药未上传处方下单、管理员访问用户接口、参数校验失败。不要只测happy path。很多同学答辩被问到“这个接口如果传了负数数量会怎样”然后现场演示直接报500印象分就没了。参数校验用Validation框架Valid NotNull Min校验失败统一捕获返回友好错误信息这个细节能显著提升代码质量和答辩表现。时间格式化也是一个高频问题。后端LocalDateTime默认序列化格式是一长串数字或带T的ISO格式前端拿到很别扭。在application.yml里配一下即可spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT84.2 Docker 宝塔部署要点毕设最终要跑起来让人看到部署环节绝对不能掉链子。最省事的方案是Docker Compose把MySQL、Redis、后端jar包三个容器编排起来。项目根目录写一个docker-compose.ymlversion: 3 services: mysql: image: mysql:8.0 ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: pharmacy volumes: - mysql-data:/var/lib/mysql redis: image: redis:5 ports: - 6379:6379 app: build: . ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/pharmacy?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai SPRING_REDIS_HOST: redis volumes: mysql-data:这里最关键的坑是docker-compose里service名直接当主机名用——后端配置数据源时数据库地址是mysql而不是localhost因为容器之间通过内网互相访问。很多同学在自己电脑上跑得好好的部署到服务器报数据库连接失败就是因为这个。前端打包后可以用宝塔面板的Nginx托管静态文件反代/api路径到后端容器端口。Nginx配置文件核心思想是这样的location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }前缀重写要特别注意前端请求/api/drug/list代理到后端的URL是/drug/list所以proxy_pass后面要加一个/来去掉/api前缀。这个细节几乎每次部署都会被问到提前记住了能省很多折腾。构建Docker镜像时Dockerfile这样写FROM openjdk:8-jre-alpine COPY target/pharmacy-0.0.1-SNAPSHOT.jar /app.jar ENTRYPOINT [java, -jar, /app.jar]4.3 常见问题速查表症状原因解决方案启动报Unable to find a SpringBootConfiguration启动类位置不对确保启动类放在源码根目录下数据库连接失败Communications link failuredocker容器内主机名错误检查是否用了localhost容器间要用service名JWT拦截器放行登录却还是403排除路径没配置拦截器注册时addExcludePathPatterns(/user/login, /user/register, /drug/**)中文乱码连接串没指定编码jdbc连接加useUnicodetruecharacterEncodingutf8下单成功但库存没扣事务未生效检查是否在同一个类中自调用要跨Service调用逻辑删除后还能查到数据TableLogic未配置实体类deleted字段加TableLogic注解时间显示8小时偏差时区未配置Jackson配置time-zone: GMT8数据库连接串serverTimezoneAsia/Shanghai5. 拓展功能与答辩亮点5.1 用药提醒与系统特色功能如果时间充裕用药提醒功能是最值得扩展的方向。它需要的表很简单reminder表id、user_id、drug_name、dosage、freq如每天三次、start_date、end_date、remind_time然后用定时任务扫描今天的提醒列表通过邮件或短信接口发提醒。考虑到毕设可能没接短信服务可以退一步用站内消息或者邮件只要能展示定时任务的实际使用场景就达到目的了。另一个可以讲出亮点的细节是订单超时取消。常规做法是Scheduled每1分钟扫一次订单表把创建时间超过15分钟且状态为待付款的订单置为已取消并回补库存。之所以把它拿出来单独说是因为它涉及一个非常微妙的业务场景MySQL的行级锁竞争。如果下单和取消同时发生可能取消任务读到旧库存后回补了一个已经扣掉的库存。解决方法是取消时加状态条件UPDATE orders SET status CANCELLED WHERE id #{id} AND status PENDING_PAYMENT受影响行数为0说明订单已经支付不再回补库存。这个细节面试官问了也不怕它展示了你对并发下数据一致性的思考。5.2 我做完这个项目后最想叮嘱的三件事第一件项目不要等全部做完再去写论文应该边做边把每个模块的截图和遇到的坑记录下来。真实踩坑经历写在论文里反而比干巴巴的原理讲述更有说服力答辩时讲“我当时遇到超卖问题后来发现是SQL里少了库存条件”比背概念自然得多。第二件系统里所有日期时间处理、金额处理都用统一规范。金额用BigDecimal而不是double日期用LocalDateTime而不是Date这两个规范能让你避免大量隐形bug。真到了答辩现场评委看代码时这两个细节很容易获得好感。第三件答辩前给自己准备一份“系统演示脚本”按照一条主线走下来注册→登录→浏览OTC药品→加购物车→模拟支付→后台审核处方→发货→完成订单。不要现场临时操作容易手忙脚乱。数据库里预先准备一批带中文数据的药品记录和几张测试订单演示效果会流畅很多。处方药审核流程也要提前走一遍这个流程环节多、状态切换多是演示的重头戏。网上购药系统每年都有无数人在做但真正做到“懂业务、有细节、能落地”的并不多。把上面这些点做好你的作品在答辩桌上就是有亮点的那一个。最后祝所有正在为毕设头秃的同学如愿通过答辩朝着996大厂又近了一步。