尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot+Vue医疗用品商城:设计与开发全流程详解

Spring Boot+Vue医疗用品商城:设计与开发全流程详解 开年到现在我已经被问了好几次“毕业设计选什么题目”和“小团队做商城从哪下手”十有八九都会提到这套组合Spring Boot做后端Vue做前端。说实话这个技术栈选得没毛病生态成熟、资料多、上手曲线也还算温和。但如果你要做的是医疗用品销售商城那情况就有点不一样了——它跟卖衣服、卖零食的通用商城有本质区别核心不在“商城”两个字而在“医疗用品”这四个字背后的合规、资质、效期、批次这些业务细节上。这篇文章我就以“springbootvue医疗用品销售商城网站”这个项目为例子完整拆一遍从业务设计、数据库建模、后端接口、前端页面到部署上线的全过程。我会把每一步为什么这么做、怎么做、踩过哪些坑都讲清楚尽量让一个零基础的人照着也能做出来也让有经验的人看了能少走几条弯路。1. 这个项目到底在做什么先想清楚再动手1.1 医疗用品商城和普通电商的五个关键差异很多人一拿到这个题目脑子里浮现的就是“商品列表、购物车、下单、支付、后台管理”这老几样。如果只做这些那这跟一个卖数码产品的商城没有任何区别答辩或验收的时候也很难讲出亮点。医疗用品商城的特殊之处至少要在以下五个方面体现出来第一商品分类的合规属性。医疗用品里有医疗器械、消毒产品、防护用品、家用检测设备等等。不同类别对商家资质有不同要求。比如二类医疗器械需要备案凭证三类器械的管理更严格。你的商城系统里应该有一个“商品资质”的概念而不是简单一个“分类”字段。最朴素的建模方式商品表里增加一个certificate_type同时单独建一张商品资质附件表存放证件图片、编号、有效期。第二效期和批次管理。这是跟普通电商最大的区别。普通电商卖一件T恤不用关心它是哪一批生产的但卖医用口罩、消毒液、敷料贴必须知道它属于哪个生产批次、有效期到什么时候。系统里必须有batch_no批次号和expiry_date有效期这样的字段并且在库存扣减时遵循“先进先出”原则。如果这个细节不做那你做的就是一个披着医疗外衣的普通商城。第三购买限制。部分医疗用品存在购买数量限制或处方药要求。虽然一般校园项目不需要真的对接医院系统但系统架构上要预留“购买限制”的扩展位。比如在商品表里加一个limit_count字段默认0表示不限购大于0时按用户维度限制总购买数量。第四售后和溯源的侧重不同。普通电商的售后重点是退换货医疗用品的售后重点还有“批次追溯”。某个批次如果质量出了问题系统要能查到哪些订单用了这个批次。所以订单明细表里除了商品ID最好还记录batch_no和expiry_date。第五物流要求。部分医疗用品对运输条件有要求比如需要冷链。这种一般在小项目里做不出来但物流模板表里可以加一个transport_condition字段算是提前留一个专业化的口子。把这五点想明白你的项目从一开始就跟那些“通用商城换个皮肤”的方案拉开了档次。这不只是给答辩加分的问题而是你真的在做一个可以落地的小型商用系统。1.2 技术选型为什么是Spring Boot加Vue而不是别的说回技术栈。为什么这个组合能成为这类项目的默认答案不是没有原因的。Spring Boot之所以被这么多人用核心是它把Spring那一大堆繁琐的XML配置全干掉了。以前搭一个Spring MVC项目要写web.xml、spring-mvc.xml、数据源配置、事务配置光是环境搭建就可能折腾两三天。Spring Boot用“约定大于配置”的思路加上自动装配机制把最常用的功能都预设好了你只需要在application.yml里写几行配置一个能跑起来的Web服务就出现了。对于中小型项目和课程设计来说这个效率优势是决定性的。而且现在互联网上的Spring Boot教程数量极其庞大从环境搭建到部署上线踩过的坑几乎都能搜到答案。Vue这边它最大的优势是渐进式。你不用一开始就把整个框架的所有概念都学完。可以先在HTML里引入一个CDN的Vue文件做简单的数据绑定等需求复杂了再用Vue Router做路由、用Pinia或Vuex做状态管理、用Vite做工程化构建。这种平滑的学习曲线对刚接触前后端分离的人来说非常友好。而且Vue的响应式机制让“数据变了页面自动变”这件事变得无比自然——你不用再像以前写jQuery那样手动去操作DOM更新页面。当然也有别的选项比如若依框架直接生成一套后台管理系统或者用Next.js做全栈。但这两条路子的问题也很明显若依的好处是啥都给你配好了坏处也是啥都给你配好了——你很难讲清楚里面每一块是怎么工作的毕设答辩时一个“若依是怎么实现权限拦截的”就能问住你。Next.js做全栈确实酷但国内很多教材、工具链、部署文档还是围绕Spring Boot加Vue这个生态来的遇到问题好解决。所以如果你现在还在纠结选型我的建议就这一句话已经有明确技术栈要求的别改没有要求的就顺着Spring Boot加Vue走这是目前综合成本最低、最稳的方案。2. 数据库设计与核心业务模型2.1 表结构设计十二张表把业务说完数据库是整个项目的骨架。我按实际开发经验把表分成“基础用户”“商品与库存”“交易链路”“营销与内容”四个组一共十二张核心表。这里直接把表结构的关键字段列出来并附上一句设计原因。基础用户组userid、手机号、密码BCrypt加密、昵称、头像、角色USER/ADMIN、状态、创建时间。前端的登录态和后台管理员的权限都靠这张表区分。user_addressid、用户id、收货人、手机号、省市区、详细地址、是否默认。订单结算时要取地址列表默认地址要优先展示。商品与库存组categoryid、父级id、名称、图标、排序。做两级分类就够了一级是医用口罩、体温计、敷料这类大类二级是更细的规格分类。productid、分类id、名称、副标题、主图、详情富文本、单位、价格、市场价、库存总量、销量、是否上架、是否推荐、限购数量、资质类型、创建时间。注意这里有一个库存总量但真正扣库存不是扣这个字段而是扣对应的批次库存这个字段只用于列表展示。product_sku可选id、商品id、规格名比如“50只装/盒”、规格值、价格、库存。如果商品有不同规格就拆到这张表。小项目里如果商品没有规格也可以不做这张表直接把规格信息塞到product表的字段里。stock_batchid、商品id、批次号、生产日期、有效期、入库数量、剩余数量。这就是前面说的医疗用品特色表核心目的是支持效期管理和先进先出。交易链路组cartid、用户id、商品id、数量、选中状态、加入时间。购物车是典型的“用户维度数据”每次加购就往这张表插一条或者累加数量。ordersid、订单号、用户id、总金额、实付金额、运费、收货信息快照、状态待付款/待发货/待收货/已完成/已取消、支付方式、创建时间、支付时间。收货信息必须做快照不然用户改了地址历史订单的地址也会变这就不对了。order_itemid、订单id、商品id、商品名称快照、商品图片快照、单价快照、数量、小计、批次号、有效期。这里单独记了批次号和有效期是为了溯源普通电商不这么干医疗用品必须这么干。payment_recordid、支付流水号、订单id、金额、支付方式、支付状态、回调内容、创建时间。接入支付宝或微信支付时回调通知就更新这张表。营销与内容组bannerid、图片、跳转链接、排序、状态。首页轮播图。noticeid、标题、内容、类型公告/活动、状态、发布时间。做站内公告栏也方便发布发货延迟通知这类运营信息。提示以上表不是最终的实际开发中你可能需要加一两个字段比如product里加一个prescription_required是否需要处方字段。但核心的十二张表逻辑是完整的照着建库不会出大问题。2.2 建表SQL示例几个关键细节直接给一份简化但能用的建表SQL片段重点关注stock_batch和order_item这两张医疗特色表。CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) NOT NULL COMMENT 分类id, name varchar(200) NOT NULL COMMENT 商品名称, subtitle varchar(500) DEFAULT NULL COMMENT 副标题, main_image varchar(500) DEFAULT NULL COMMENT 主图, detail text COMMENT 详情富文本, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 市场价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存总量冗余, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 上架状态0下架 1上架, limit_count int(11) NOT NULL DEFAULT 0 COMMENT 限购数量0不限购, certificate_type varchar(50) DEFAULT NULL COMMENT 资质类型1类/2类/3类, create_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE stock_batch ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL, batch_no varchar(100) NOT NULL COMMENT 生产批次号, produce_date date DEFAULT NULL, expiry_date date NOT NULL COMMENT 有效期, quantity int(11) NOT NULL COMMENT 入库数量, remaining int(11) NOT NULL COMMENT 剩余数量, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT批次库存表;价格字段必须用decimal(10,2)不要用float或者double不然算金额会出现精度问题。这个我之前踩过坑用double存价格订单总额算出来是19.999999这种数前端展示和支付都会出问题。详情富文本用text类型就够了不需要longtext。时间字段建议用datetime别用timestamp因为timestamp有2038年问题而且受时区影响容易出莫名其妙的时间差。2.3 订单表的状态机设计订单状态这块很多新手会做成一个status字段随便填这是不对的。订单状态应该是有明确流转路径的。前端要根据状态显示不同按钮后端要根据状态决定能不能执行某个操作。我建议用以下状态流转状态0待付款。用户下单后未支付。后端在创建订单时生成同时记录create_time方便后续的定时任务关闭超时未支付订单。状态1待发货。用户支付成功后支付回调里把订单状态从0改成1。状态2待收货。管理员在后台发货填物流单号把订单状态从1改成2同时记录ship_time。状态3已完成。用户点击确认收货系统自动把状态改成3。状态4已取消。用户主动取消待付款状态下或超时关闭。状态机设计最关键的一点是任何一步的状态变更都要校验前置状态。比如“已取消”的订单不能被改成“待发货”“已完成”的订单不能被再次取消。最简单的方式是在Service层里挨个判断代码多一些但逻辑清晰也可以用设计模式里的状态模式去优化但小项目里没必要过度设计if判断就够用了。3. 后端开发Spring Boot的核心实现3.1 项目的初始化与依赖管理创建Spring Boot项目的方式很多Idea里有Spring Initializr也可以直接去Spring官网的start.spring.io页面生成。我的建议是直接去start.spring.io下载选好依赖再导入Idea这样版本不容易出问题。依赖这块如果按最简单的来至少要有dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency这里有几个为什么。为什么用MyBatis-Plus而不是原生MyBatis因为单表CURD几乎不用写XML内置的BaseMapper已经把增删改查、分页、条件构造全封装好了。团队写代码的效率高很多对新手也友好。为什么用Lombok因为实体类里的getter、setter、toString这些模板代码太啰嗦了用Data注解一行就全解决了代码能少写一半。还有一个经常被忽略的配置application.yml里的日期格式和Jackson配置。前端拿到的数据如果是Date类型的字段默认会序列化成一个时间戳或者一串带T的UTC格式很难看前端还得自己转格式。最好的方式是在配置里统一格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果不加time-zone: GMT8后端返回的时间会比北京时间少8个小时排查起来很诡异。建议这一步从项目一开始就配上别等前端同事过来找你。3.2 为什么Spring Boot的自动装配这么好用浅谈原理提到Spring Boot就绕不开“自动装配”这四个字。很多教程讲得太深入什么EnableAutoConfiguration、spring.factories、Conditional注解这些术语一套一套的新手听完就懵了。我用一个生活化的类比来解释一下。你把Spring Boot看成是一个精装房交付的装修公司。你只需要告诉它“我要装成一室一厅”引入依赖装修公司会根据房间面积、户型、水管电路这些条件自动把该装的东西装好——水管接口处自动接好、电表箱自动配好漏保、墙漆自动刷好底漆。这些就是在spring-boot-autoconfigure这个jar包里预设好的“配置逻辑”。Spring Boot在启动时会扫描这些已有的自动配置类然后根据你的实际依赖情况用ConditionalOnClass这样的注解去做条件判断如果你类路径下有Redis的jar包且你没手动配置RedisTemplate那它就给你配一个默认的如果你有数据源配置那它就把DataSource和JdbcTemplate配好。所以你用Spring Boot时的感觉就是“咦我好像啥都没写程序就能跑了”。原理其实就是Spring Boot内部预置了大量默认行为且只有在你满足某些条件时才触发这些默认行为。理解到这一层就够了写业务代码不需要再深挖。3.3 登录鉴权和拦截器写一个可复用的JWT流程商城系统肯定要有登录功能。这里我用JWTJSON Web Token做用户认证不搞Session因为前后端分离之后后端接口是无状态的不能假设前端一直保持同一个Session。JWT的基本思路是用户登录成功后后端生成一个带签名和过期时间的Token返回给前端前端之后每次请求都在HTTP头里带上这个Token后端通过拦截器解析Token识别出当前用户是谁。在Spring Boot里实现这个流程一共分四步第一步写一个JWT工具类。负责生成Token和解析Token内部用HMAC256算法签名。我这里不多贴代码了核心逻辑就是用JWT.create()放入用户id和过期时间然后用密钥签名。第二步写一个拦截器继承HandlerInterceptorAdapterSpring 5.3之后可以改实现HandlerInterceptor在preHandle方法里从请求头拿Token解析失败就返回401解析成功就把用户id存到ThreadLocal或RequestContextHolder里方便后续的Service层直接获取当前登录用户。第三步注册拦截器。在WebMvcConfigurer里给addInterceptor方法配置放行的路径。公开接口登录、注册、商品列表、商品详情放行私有接口下单、购物车、我的订单全部拦截。第四步写一个登录接口。调用AuthenticationManager或者直接从数据库查用户小项目不需要整合Spring Security密码用BCryptPasswordEncoder做校验匹配成功就生成Token返回。这里有一个特别需要注意的坑拦截器拦截了OPTIONS预检请求。在前后端分离开发中前端跨域请求会先发一个OPTIONS类型的预检请求。如果你在拦截器里直接拦截这个请求就会导致前端每次请求都失败提示跨域。解决方法是在拦截器里判断如果请求方法是OPTIONS就直接放行返回200。这个问题非常隐蔽我见过不少人卡在这里一整天。另外一个通用技巧是写一个全局异常处理器用ControllerAdvice和ExceptionHandler统一处理异常。这样后端代码里就不用到处写try...catch抛出业务异常后由统一的地方处理返回{ code: 500, msg: 库存不足 }这样的JSON结构。前端拿到这个统一结构弹个message.error(msg)就行。这个设计带来的好处是前后端联调效率大幅提升不用为了每个接口的错误格式单独写逻辑。3.4 商品与订单核心接口的设计思路商品模块的核心接口是分页查询。不做分页的话几百条数据全量返回前端渲染都卡。分页这一块MyBatis-Plus做了很友好的封装只需要配一个分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后查询接口里通过PageProduct对象传入当前页和每页数量即可返回结果里自带总数、总页数这些信息。订单模块的核心是下单事务。下单这个操作不是简单地把数据往表里插一下它至少要经历以下几步查询购物车或前端传过来的商品id和数量。校验商品是否存在、是否上架、库存是否充足。计算订单总金额。扣减库存。生成订单主表和订单明细。清空购物车。这些操作要么全部成功要么全部失败绝对不能出现“订单生成了但库存没扣减”这种数据不一致的问题。Spring Boot里实现多表操作一致性比较简单的方式是在Service方法上加Transactional注解事务管理交给Spring处理。这里我在实际开发中踩过一个教训扣库存一定要先查再扣并且要校验剩余量不低于0。如果不加校验在并发情况下会出现超卖——10个库存被卖了12单。最简单的做法是在更新库存的SQL语句里带上条件remaining 购买数量更新影响行数为0就说明库存不够直接抛出异常回滚事务。在代码层面虽然不能完全解决超卖但这种原子更新配合数据库行锁已经能应对绝大多数中小流量的场景了。3.5 首页聚合接口用一个Controller合并数据首页要展示的内容通常包括轮播图、公告、推荐商品、分类导航。如果前端分别调4个接口页面加载会慢而且逻辑比较零散。更好的做法是后端写一个getHomeData接口一次性把首页需要的所有数据查询出来封装到一个HomeVO对象里返回给前端。public class HomeVO { private ListBanner banners; private ListNotice notices; private ListCategory categoryList; private ListProduct hotProducts; private ListProduct newProducts; }这个接口的设计看起来很简单但实际开发中很实用。前端只需要调一次而且可以在后端查询时做缓存比如加Redis首页加载速度会明显提升。这种“针对页面聚合数据”的思路比一个页面调多个接口的通用做法更贴近实际工程场景也更好讲清楚自己接口设计的逻辑。4. 前端开发Vue的实现要点4.1 Vue 3加Vite项目初始化和目录结构Vue这一侧我建议直接用Vue 3加Vite不要再学Vue 2了。Vue 3的组合式APIComposition API比Options API在逻辑复用和代码组织上清晰太多Vite的冷启动速度秒杀Webpack开发体验完全是两个时代。另外还要注意Vite的Node.js版本要求vite 5需要Node.js 18以上vite 4需要Node.js 14.18以上。如果你用的是公司老电脑Node版本还停留在12那装依赖的时候会收到一堆引擎警告甚至直接报错。解决办法就是升级Node用nvm管理Node版本最方便。创建项目直接一行命令npm create vitelatest medical-mall-front然后根据提示选择Vue和TypeScript。这里我多说一句要不要上TypeScript如果你对TS不熟可以选JS版本项目结构更简单Hack门槛更低。但如果你有一点类型基础还是建议用TS因为Vue 3配合TS做项目的时候IDE提示和代码可维护性都会好很多。目录结构我习惯这样组织src/ api/ -- 各模块的接口请求封装 assets/ -- 静态资源 components/ -- 通用组件 router/ -- 路由配置 store/ -- 状态管理Pinia views/ -- 页面组件 utils/ -- 工具函数如request.js封装axios4.2 Vue Router与路由守卫未登录跳转登录页商城系统里有两种角色普通用户和管理员。普通用户的操作范围是浏览商品、下单、查看订单管理员的操作范围是后台管理页面。因此在路由设计上前后台的路由应该分开。前台路由大致是/home首页/category商品分类页/product/:id商品详情页/cart购物车页/order/confirm订单确认页需要登录/order/list我的订单页需要登录/login、/register登录注册页后台路由需要管理员权限/admin/dashboard数据看板/admin/product商品管理/admin/order订单管理/admin/category分类管理Vue Router本身只是一个路由映射表真正控制访问权限的是路由守卫。在router/index.js里配置一个全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这里有个进阶的小技巧如果项目未来要做动态权限比如不同管理员看到不同菜单路由守卫只做第一层校验菜单的显示与否要根据用户角色动态生成这就是热词里提到的“Vue动态路由”。小项目先不管动态路由把基础的requiresAuth守卫做好再加一个meta.role字段判断管理员页面即可。4.3 Axios封装和Token携带前端跟后端交互的核心工具是Axios不要每个页面里直接axios.get(...)应该封装一个统一的request.js。好处有两点一是公共配置只写一次二是响应拦截器可以统一处理登录失效和错误提示。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.response?.data?.msg || 请求失败) } return Promise.reject(error) } ) export default request这样封装之后业务代码里调接口就变得非常简洁import request from /utils/request export const getHomeData () { return request.get(/home) }响应拦截器里的401处理是重点项目。后端Token过期后返回401前端自动清理本地Token并跳转登录页这样用户操作到一半才发现需要重新登录的体验就会好很多不会一直刷页面报错。4.4 页面与组件设计商品详情页和后台CRUD商品详情页是前端的一块硬骨头需要处理的交互有商品图片展示、规格选择、数量增减、加入购物车、立即购买。建议把这个页面拆成多个子组件来做ProductInfo基本信息、ProductGallery图片区、ProductSpec规格选择、CartBar底部操作栏。组件拆分的好处是每个组件的职责单一调试和替换都方便。购物车页面则要处理“选中商品才计算总价”“全选、反选、单选的总价联动”这些逻辑。这里特别提醒一个Vue新手常犯的错误直接对cartList里的item.checked做赋值页面不更新。在Vue 3里如果数据是用reactive包装的普通赋值是可以被拦截到的但如果用了ref且没有通过.value赋值或者直接操作了嵌套对象的新增属性就容易出现响应式失效。最简单的方法是在初始化购物车列表时就把每个商品都带上checked: true字段不要后面再加。后台管理页面的核心是表格和表单。列表页用el-table通过:data传入后端分页数据新增和编辑用el-dialog加el-form。这里要注意一点编辑时表单回填的数据不要直接把原对象赋给form否则你改了表单列表行里的数据也会跟着变因为引用同一个对象。正确做法是form { ...row }做一层浅拷贝或者用Object.assign({}, row)。5. 前后端联调与常见“坑”排查实录5.1 跨域问题开发环境和生产环境的不一样处理方式前后端分离开发跨域是一场绕不开的仗。后端跑在localhost:8080前端跑在localhost:5173端口不一样浏览器就会拦截请求控制台报经典的“CORS”错误。开发环境的解决办法有两个。一是后端配CrossOrigin注解或者全局CORS配置。如果你只想在开发阶段用后端配一个就已经解决了。如果你装了Nginx或者用了代理也可以在前端的vite.config.js里配置代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/home会被Vite开发服务器转发到http://localhost:8080/api/home。之所以推荐第二种是因为它能模拟生产环境的同源场景同时避免把后端接口地址暴露在浏览器的Network面板里。生产环境的跨域问题不要在后端解决应该交给Nginx。Nginx反向代理可以把前端静态资源和后端接口放在同一个域名下从根上消除跨域。这个方案比后端一个个接口配CORS要稳得多一个location ^~ /api/就把接口请求全部转发到Spring Boot进程location / { root /opt/mall-front/dist; index index.html; try_files $uri $uri/ /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; }这里try_files $uri $uri/ /index.html很重要因为Vue Router用的是history模式刷新或直接访问一个子路由路径时Nginx需要把请求重写回index.html否则会404。5.2 数据精度问题后端Long类型传到前端丢失精度这是Java后端最经典的一个坑。数据库里的主键id一般用bigintJava里对应Long类型但JavaScript的Number类型最大安全整数是2^53 - 1恰好比Java的Long小那么一点。当id超过这个范围时前端拿到的数字会变成不精确的值比如订单号157xxxxxxx958被解析成157xxxxxxx000后面几位全变0然后你用这个id去查详情就查不到了。解决办法有三种。最省事的一种是在后端配置Jackson把Long类型全局序列化为字符串Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; } }前端拿到的id就是字符串进路由参数时就不存在精度丢失问题。第二种办法是前端把id统一当字符串处理不参与加减法运算但这种方式在接口返回大数字时还是会遇到同样问题所以要靠后端先转好。第三种是修改主键生成策略不用数据库自增改用雪花算法生成的字符串主键但这种方案要引入额外组件小项目没必要。5.3 登录Token失效和拦截器顺序问题在实际运行中Token失效是常见故障。表现是用户在前端页面操作一段时间后点付款或点提交订单突然被弹回登录页。用户以为是自己掉线了但其实是因为Token过期了。解决方案有几层一是把Token过期时间设长一点比如7天。二是做一个无感刷新机制前端每次请求之前检查Token剩余有效时间如果快过期了先请求一个refresh-token接口换新Token再发起业务请求。三是后端在拦截器里对“已过期Token”和“无Token请求”做不同处理无Token返回401跳登录已过期返回特定错误码前端看到后自动调刷新接口。小项目可以先做第一种简单直接如果要做得好一点加一层刷新机制也不难。另外一个后端经常出问题的细节是拦截器的执行顺序。如果你同时配置了多个拦截器比如LoginInterceptor和LogInterceptor要确认它们的执行顺序符合预期。Spring Boot里新增多个拦截器的顺序是注册顺序先注册的先执行。如果你把权限拦截器放在日志拦截器后面那未登录请求就不会被日志记录排查问题时会很困惑。5.4 商品图片存储本地路径还是MinIO对象存储商品系统肯定要传图片的这里得讲清楚一个问题图片存哪最简陋的方案是把图片上传到本地磁盘然后在数据库里存一个/tmp/xxx.jpg这样的路径。这个方案在开发阶段能用但一部署到服务器上就会遇到两个问题一是路径权限搞错导致图片无法读取二是重启或者换服务器时图片跟着丢了。稍微规范一点的做法在服务器上建一个固定的upload目录比如/home/ubuntu/uploads/在Spring Boot配置里把目录配置成可配置项并加一个静态资源映射把/images/**映射到这个磁盘目录。这条链路能跑通但还没解决“图片备份和分发”的问题。更专业的做法是引入MinIO。MinIO是一个开源的对象存储服务兼容S3协议可以部署在自己服务器上。图片上传到MinIO数据库里只存访问路径。MinIO的好处是容量可以横向扩展可以配合CDN做分发也可以在后端做桶的权限配置。把MinIO集成进Spring Boot也不难引入minio-java依赖配好endpoint、accessKey、secretKey然后写一个上传服务类。在Java里两步一项是MinioClient.builder()另一项是minioClient.putObject()。如果项目里已经有MinIO部署建议直接用如果只是为了这个项目再搭一套可以先本地磁盘方案但要在代码里预留好存储层接口方便后续替换。6. 常见面试问题与避坑指南6.1 Spring Boot与Vue面试高频考点整理如果你这是毕设项目答辩时一定会被问到几个固定问题。这里我提前把答案要点列出来。问题一Spring Boot自动配置的原理。回答思路是Spring Boot启动时通过SpringBootApplication中的EnableAutoConfiguration开启自动配置内部会通过AutoConfigurationImportSelector加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件读取所有自动配置类然后根据类路径上是否存在对应的依赖以及你是否手动定义过相关Bean结合Conditional系列条件注解决定是否加载这个自动配置。最终这些配置类会注册一批Bean到容器中你就能直接注入了。问题二拦截器和过滤器有什么区别。回答思路是过滤器是Servlet规范的一部分作用于所有请求在进入DispatcherServlet之前执行拦截器是Spring MVC的组件只能拦截经过DispatcherServlet的Controller请求。过滤器和拦截器的执行顺序是请求先经过过滤器再到拦截器的preHandle执行Controller然后经过拦截器的postHandle和afterCompletion最后返回响应。在项目中用拦截器做登录验证是因为可以精确控制哪些路径放行、哪些路径拦截还能通过HandlerMethod获取方法上的注解。问题三Vue的响应式原理是什么。回答思路是Vue 3使用Proxy代理对象在读取属性时收集依赖在修改属性时触发更新。依赖收集的核心是effect函数组件更新函数就是一个effect。Vue 2使用Object.defineProperty只能拦截已有属性新增属性需要Vue.setVue 3的Proxy可以直接拦截新增和删除属性这是Vue 3更适合现代前端开发的原因之一。问题四JWT和Session有什么区别。回答思路是Session存在服务端内存或Redis中客户端只保存SessionId每次请求都要去服务端查询。JWT是无状态的用户信息加密保存在Token里服务端不需要保存状态。JWT的优点是天然适合前后端分离和分布式部署缺点是难以主动吊销除非维护黑名单或缩短过期时间并且Token体积比较大。6.2 避坑记录我做过这个项目后总结的经验这一节直接分享几个我实际开发中遇到过的、有代表性的问题。第一个坑是订单金额用浮点数计算。我发现有些同学为了省事金额字段不建decimalJava里用double最后算出来如果是19.999999比较大小和支付时都会出问题。钱的问题必须用BigDecimal或者数据库decimal这是一个不容商量的技术选型。第二个坑是写了事务但没有生效。加个Transactional注解就能开启事务这是Spring Boot给人的错觉。实际上Transactional默认只对RuntimeException回滚如果你在Service方法里捕获了异常并当成了普通分支处理事务就不会回滚。我遇到过有人在下单方法里catch了异常又没抛出结果订单表和新插入的明细成了“半成品”数据完全对不上。正确的做法是业务异常都抛出去由全局异常处理去接。第三个坑是前端发请求时没处理响应拦截器返回的结构。如果你的响应拦截器返回的是response.data那业务代码里const res await getUser(); res.code才是后端返回的业务码。有些同学忘了这层直接res.data.code结果取到的undefined排查半天也不知道为什么。这种事看起来低级但实际联调中几乎每个人都遇过。统一封装后去查看请求返回的字段结构养成习惯就好了。第四个坑是时间字段序列化之后前端显示乱码。数据库里明明存了2025-03-01 10:30:00传到前端后变成了Mar 1, 2025或者一串时间戳。解决方法前面说过了后端统一配置Jackson的日期格式为yyyy-MM-dd HH:mm:ss。这个配置一定要加越早加影响面越小。7. 从项目到可部署状态打包与上线7.1 后端打包和前端构建项目做完以后不可能一直让它跑在Idea或开发服务器里得把它变成能部署到服务器上的产物。后端用的是Maven在项目根目录执行mvn clean package -DskipTests这个命令会在target目录下生成一个xxx.jar。注意一定要加-DskipTests不然每次打包都会自动跑一遍单元测试如果测试代码跟环境不匹配还会直接编译失败。前端用Vite构建npm run build构建完成后产物在dist目录里面是纯静态文件index.html、assets目录等等。把dist里的内容整个上传到服务器的/opt/mall-front/dist目录即可。7.2 部署方案服务端运行和进程守护部署的完整流程我建议按这个顺序来服务器装好JDK 17Spring Boot 3要求JDK 17以上Spring Boot 2只需要JDK 8。如果你用的是Spring Boot 3那要注意JDK版本不要装错了。装好MySQL并建库导入init.sql确保数据库账号密码跟application.yml里的配置一致。装好Nginx配置前端静态资源目录和反向代理。把后端jar包上传到服务器的/opt/mall-server/目录。启动后端服务nohup java -jar medical-mall.jar mall.log 21 。很多同学跑到第5步就完事了但这其实只是一个临时启动方式。一旦服务器重启进程就没了。规范的服务器部署方式要用systemd做进程守护。写一个/etc/systemd/system/mall.service文件[Unit] DescriptionMedical Mall Server Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/opt/mall-server ExecStart/usr/bin/java -jar medical-mall.jar Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后执行systemctl daemon-reload和systemctl enable mall这样开机自启和异常重启都处理好了。7.3 上线前必须检查的几件事上线前这几件事没做后面一定会回来找你麻烦。第一application.yml里的数据库密码不要用弱口令也不要把密码硬编码到Git提交里。小项目可能无所谓但你养成这个习惯以后会有好处。用环境变量覆盖配置是更规范的做法spring.datasource.password: ${DB_PASSWORD}然后服务器启动时设置好环境变量。第二检查文件上传目录的权限。如果你用了本地存储方式上传商品图片upload目录的属主和权限要对否则图片传上去写不进去或者写进去了Nginx读不到。第三确保后端接口和前端路由的匹配。比如前端请求/api/home后端Controller的路径就必须是/home如果配置了server.servlet.context-path/api或者前端baseURL补全成/api。第四部署后一定要测试一遍完整的用户流程注册用户、登录、浏览商品、加购物车、下单、模拟支付、后台发货、确认收货。这个流程走通了你才能放心地把系统交出去。最后分享一个我自己的经验整个项目从零开始动手到落地上线我印象最深的一个经验就是别一上来就写代码先花两天把数据库表设计好把接口文档列出来后面会顺很多。表面上看你比别人晚动手实际上后期改来改去的成本远低于一开始就“先写起来再说”的做法。就拿医疗用品商城的批次管理来说如果没有事先设计好stock_batch表后面再去补效期逻辑时就必须回头改商品表、订单表、库存扣减逻辑牵一发动全身改到怀疑人生。还有一个小建议前端在开发阶段建议多写弹窗确认和加载状态这些细节虽然不影响核心功能但会让演示或答辩时的用户体验明显更好。比如加入购物车成功后的轻提示、下单成功后的跳转倒计时、后台编辑商品时的加载遮罩都能让你的系统显得“做完了”而不是“能跑而已”。这个项目做完之后如果你想继续扩展有三个方向可以参考一是接入真实的支付支付宝沙箱环境就够不需要商户号也能调通二是接短信服务做注册验证码三是用WebSocket做一个客服聊天或通知中心。每一条都是真实业务可能需要的能力也能让你的项目在众多“能跑”的毕设里更有辨识度。
返回列表