ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue渔具商城系统从零到上线全解析:数据库设计、JWT认证与部署实战

SpringBoot+Vue渔具商城系统从零到上线全解析:数据库设计、JWT认证与部署实战 做过“基于SpringBootVue的XX管理系统”这类课题的同学应该都有同感题目拿到手最怕的不是不会写代码而是不知道从哪下手。今天写的这个“垂钓渔具店网上商城购物管理系统”乍看是常见的电商CRUD但真正落地时会发现从渔具商品的规格参数设计到下单时的库存扣减再到Vue前端的状态管理每一环都藏着不少细节。如果你正在做类似的毕设或全栈练习项目这篇文章可以当作一份“从零到上线”的完整参考。我会把系统的核心设计思路、数据库表结构、后端接口实现、前端Vue业务开发以及前后端联调和部署过程中踩过的坑按模块拆开讲清楚。项目使用的技术栈是SpringBoot Vue MySQL MyBatis-Plus这也是目前中小型管理系统非常主流的组合做完这一个项目你对前后端分离开发的整套流程基本就有底了。1. 项目概览与技术选型为什么是SpringBoot Vue1.1 垂钓渔具垂直电商的业务特殊性很多人一听到“网上商城”第一反应就是照着淘宝京东抄一套。但真去做了会发现通用商城的模板放到渔具行业里很多地方是水土不服的。渔具这个品类SKU比较复杂。一根鱼竿可能有“长度2.7米/3.6米/4.5米”、“调性28调/37调”、“材质碳素/玻璃钢”这些维度一套鱼线还分线号、长度、颜色鱼饵更是品牌、味型、重量五花八门。所以在设计商品表时不能只放一个简单的price和stock字段还要考虑商品规格或者说SKU的存法。同时渔具消费有明显的季节性和地域性春季野钓旺季、夏季夜钓热门这些都是业务层面要留意的点但在系统设计上核心还是先把商品、购物车、订单这条电商主链路跑通。因此这个系统的定位就很明确了面向渔具店经营者提供商品管理、订单管理、会员管理、数据统计等后台功能面向钓鱼爱好者提供商品浏览、搜索、购物车、下单、订单查询等前台功能。目标用户是两类角色系统天然要拆成“前台商城”和“后台管理”两部分这也决定了前后端分离的架构方向。1.2 技术栈选型的逻辑与版本取舍先列一下我用到的技术栈层次技术选型说明后端框架SpringBoot 2.7.x稳定、生态成熟资料多适合快速搭建ORMMyBatis-Plus单表CRUD几乎不用写SQL分页插件好用数据库MySQL 5.7 / 8.0关系型数据满足商城事务一致性鉴权JWT无状态登录前后端分离场景标配前端框架Vue 3 Vite组合式API写业务更顺手Vite冷启动快UI组件库Element Plus中后台管理界面的利器状态管理PiniaVue 3官方推荐比Vuex更简洁HTTP工具Axios统一请求拦截器处理token和错误码为什么后端选SpringBoot因为对于这类管理系统SpringBoot的自动配置让项目初始化成本极低内嵌Tomcat免去单独部署容器配合Maven依赖管理一个web项目几分钟就能跑起来。对比SSHSpring MVC Spring Hibernate那套老古董SpringBoot在开发效率上的提升是肉眼可见的。为什么前端选Vue而不是React如果你是学生或者刚接触前后端分离Vue的学习曲线相对平缓模板语法直观而且Element Plus组件库把表格、表单、弹窗、分页这些后台管理系统高频组件都封装好了。Vue 3的setup语法糖写起来也非常接近原生JavaScript没有Vue 2那种繁琐的OptionAPI。版本这里多说一句SpringBoot别一上来就追最新版。我见过不少同学用SpringBoot 3.x配MyBatis-Plus结果遇到javax改成jakarta包名、部分第三方starter没适配的问题排查半天才发现是版本兼容性的锅。建议大家用2.7.x这个成熟版本前端Node也别装太新的LTS避免Vite和Node版本冲突。2. 数据库设计商城系统的地基2.1 核心表结构与字段设计数据库设计是整个系统的地基。我见过很多代码写得还不错结果一看表结构商品表里塞了一个JSON字符串存全部规格订单表里把商品名称价格全冗余进去——这不是不行但维护起来会非常痛苦。按照常规电商系统这套渔具商城至少需要这几张表用户表userid、username、password、nickname、phone、avatar、role、status、create_time、update_time、deleted。role字段区分“管理员”和“普通用户”这里用字符串或tinyint都可以建议用tinyint配合枚举方便扩展。商品分类表categoryid、name、parent_id、sort、icon、status。渔具分类可以做两级比如一级分类“鱼竿”二级分类“手竿/海竿/路亚竿”。商品表goodsid、category_id、name、subtitle、main_image、detail_images、price、stock、sales、is_hot、status、create_time、update_time、deleted。price用decima(10,2)避免浮点精度问题。商品的规格参数goods_paramid、goods_id、param_key、param_value。这一张表是渔具商品差异化的关键。鱼竿的“长度/调性/材质”鱼线的“线号/线长”都可以用参数键值对的方式存。查询详情时按goods_id取出来拼成规格列表。购物车表cartid、user_id、goods_id、goods_name、price、image、count、checked、create_time、update_time。购物车冗余商品名称、价格、图片是为了列表页少一次联表查询。订单表ordersid、order_no、user_id、total_amount、pay_amount、freight_amount、status、receiver_name、receiver_phone、receiver_address、pay_time、deliver_time、create_time、update_time。order_no要唯一用于后续支付对账。订单项表order_itemid、order_no、goods_id、goods_name、goods_image、price、count、total_price。订单生成时把商品快照写入这样一来就算商品后续改价下架历史订单里的数据仍然准确。这里要特别提醒订单表设计时一定要做商品快照也就是在订单项表里冗余商品名称、图片、价格。后续打印快递单、处理售后纠纷、做经营数据分析全靠这层冗余。否则哪天商品下架了订单记录就变成一堆看不懂的ID了。2.2 关键字段设计与索引优化开发这个系统时字段类型的选择看似不起眼实际坑不少。我列几个容易踩坑的点。价格字段坚决不用float/double用decimal(10,2)。浮点数在比较运算时会有精度丢失订单金额这种敏感数据不能有半点误差。状态字段用tinyint配合常量类或者枚举类管理状态值。比如商品状态0下架、1上架订单状态0待付款、1待发货、2待收货、3已完成、4已取消。别直接存中文否则后续扩展和统计都会很别扭。逻辑删除MyBatis-Plus支持逻辑删除配置一个deleted字段默认0删除时自动更新为1。删除商品不真的把记录从表里干掉这对于保留历史订单引用关系非常重要。索引主键索引是默认有的额外再关注这几条高频查询路径——goods.category_id按分类翻列表、orders.user_id用户查自己的订单、orders.order_no按单号精确查、cart.user_id用户查购物车。加索引时用短字段优先比如分类ID这种int类型就很合适。2.3 MyBatis-Plus自动建表与字段映射很多同学拿到项目后在本地建表建得挺开心一换电脑或者部署到服务器数据库表没了又得手动重新执行SQL一遍。热搜词里有条“springboot mybatis 当表不存在自动建表”这个需求常见于部署环节。最简单靠谱的做法是在application.yml里配置SpringBoot启动时自动执行初始化SQL脚本spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql >-- schema.sql 中建表语句示例 CREATE TABLE IF NOT EXISTS user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL, password varchar(128) NOT NULL, nickname varchar(64) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint(4) DEFAULT 0 COMMENT 0普通用户 1管理员, status tinyint(4) DEFAULT 1 COMMENT 0禁用 1正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(4) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个关键词IF NOT EXISTS。它会判断表是否存在存在就跳过不存在才创建配合SpringBoot启动时自动执行就实现了“自动建表”。不过要注意这种方式只建表不会帮你做字段变更比如你给表加了新字段旧的建表语句不会自动补上。此时最稳妥的办法是写一个版本化的升级SQL或者删表重建仅限开发环境。另一个高频坑是驼峰映射。Java实体类用驼峰命名goodsName数据库字段一般用下划线goods_name如果没配置映射规则查出来的数据一直是null。在application.yml里加一行mybatis-plus: configuration: map-underscore-to-camel-case: true这一行是MyBatis的经典配置打开后自动完成goods_name到goodsName的映射能省掉无数个TableField注解。3. 后端核心模块实现从登录到下单3.1 项目初始化与目录分层后端项目的结构直接决定这个系统后续能不能维护。我用了最经典的分层结构com.fishing.mall ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // 数据访问层MyBatis-Plus Mapper ├── entity // 实体类 ├── dto // 入参对象 ├── vo // 出参对象 ├── common // 返回结果、异常处理、常量 ├── config // 配置类拦截器、跨域、文件上传等 ├── utils // 工具类 └── exception // 自定义异常controller层只做参数接收和结果返回不写业务逻辑service层写核心业务比如下单、扣库存、购物车操作mapper层连数据库。每一层各司其职后面加功能改需求时定位问题会非常快。依赖方面pom.xml里核心就是这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.20/version /dependency其中Hutool是一个特别好用的工具类库生成订单号、ID、加密、日期处理这些都有现成方法比自己在网上Copy一堆代码片段省心得多。当然用它的前提是你了解内部实现面试被问到了不至于答不上来。3.2 用户登录与JWT认证前后端分离后Session这套方案用起来很别扭因为前端和后端不同端口跨域环境下服务端的Session不一定能种到浏览器。业界主流做法是用JWT做无状态认证。原理一句话总结用户登录成功后服务端生成一个带签名的token返回给前端前端每次请求在请求头里带上Authorization: Bearer token后端拦截器负责校验、放行或拦截。登录接口的核心逻辑Service public class UserServiceImpl implements UserService { Override public String login(LoginDTO dto) { // 1. 根据用户名查用户 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, dto.getUsername()); User user userMapper.selectOne(wrapper); // 2. 校验用户是否存在、密码是否正确 if (user null || !SecureUtil.md5(dto.getPassword()).equals(user.getPassword())) { throw new BizException(用户名或密码错误); } // 3. 生成JWT MapString, Object claims new HashMap(); claims.put(userId, user.getId()); claims.put(role, user.getRole()); String token JwtUtil.createToken(claims, 7 * 24 * 3600 * 1000L); return token; } }密码加密这里用MD5加盐或者BCrypt都可以建议用BCryptspring-security-crypto包里有现成的BCryptPasswordEncoder安全性比纯MD5高。如果项目里不想引入Spring Security全家桶单独引这个工具包也行。生成token之后需要注册一个拦截器拦截所有需要登录才能访问的请求。拦截器里做两件事第一检查请求头里有没有token第二解析token把userId放进ThreadLocal或者放到请求参数里后面业务代码直接从ThreadLocal拿当前登录用户。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StrUtil.isBlank(token) || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } Claims claims JwtUtil.parseToken(token.substring(7)); // 把userId放入request方便后续使用 request.setAttribute(userId, claims.get(userId)); return true; } }然后在WebMvcConfig里注册拦截器同时放行登录注册接口和前端静态资源否则连登录页面都进不去Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/goods/**); } }这里/api/goods/**放行是因为商品浏览是公开的游客也能看。3.3 商品、购物车与下单接口实现商品模块是系统最基础的模块。列表页需要一个分页条件查询接口支持按分类筛选、按关键字搜索、按销量或价格排序。MyBatis-Plus的Page类配合LambdaQueryWrapper写起来非常简洁GetMapping(/page) public ResultIPageGoodsVO pageGoods(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer categoryId, RequestParam(required false) String keyword) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Goods::getCategoryId, categoryId) .like(StrUtil.isNotBlank(keyword), Goods::getName, keyword) .eq(Goods::getStatus, 1) .orderByDesc(Goods::getSales); IPageGoods goodsPage goodsService.page(page, wrapper); return Result.success(goodsPage); }购物车模块相对简单无非是增删改查。这里有一个设计取舍购物车数据存前端还是存后端如果你做的是纯演示系统可以存前端本地比如Pinia里维护一个数组但更完整的方案是存后端数据库表这样用户在手机和电脑上登录购物车数据是同步的。我采用的是后端的方案购物车接口按照userId来区分用户。下单流程是整个系统的核心也是面试官最爱追问的地方。步骤如下接收前端传过来的商品ID列表和数量。循环校验每个商品的库存是否充足。生成唯一订单号规则一般是yyyyMMddHHmmss 6位随机数Hutool的IdUtil.getSnowflakeNextIdStr()也可以。计算订单总金额从购物车快照中取价格计算。事务性扣减库存。保存订单主表和订单明细表。删除当前用户购物车中对应商品。这里必须加上Transactional注解保证“扣库存”和“创建订单”是同生共死的原子操作。一旦中间任何一步抛异常要么全成功要么全回滚绝对不能出现订单创建了但库存没扣或者库存扣了但订单没影了的情况。Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, ListCartItemDTO items) { // 1. 计算总价 BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItemDTO item : items) { Goods goods goodsService.getById(item.getGoodsId()); if (goods null || goods.getStatus() ! 1) { throw new BizException(商品不存在或已下架); } if (goods.getStock() item.getCount()) { throw new BizException(商品[ goods.getName() ]库存不足); } totalAmount totalAmount.add(goods.getPrice().multiply(new BigDecimal(item.getCount()))); // 构造订单项快照 OrderItem orderItem new OrderItem(); orderItem.setGoodsId(goods.getId()); orderItem.setGoodsName(goods.getName()); orderItem.setGoodsImage(goods.getMainImage()); orderItem.setPrice(goods.getPrice()); orderItem.setCount(item.getCount()); orderItem.setTotalPrice(goods.getPrice().multiply(new BigDecimal(item.getCount()))); orderItems.add(orderItem); } // 2. 生成订单号 String orderNo IdUtil.getSnowflakeNextIdStr(); // 3. 扣库存、存订单... }订单状态的流转我用一个枚举常量类管理后端接口的职责就是保证状态的合法迁移。比如已取消的订单不能再支付已支付的订单不能重复支付已发货的订单不能取消。这些状态机逻辑虽然在当前项目里不算复杂但写规范一点后续接支付、接售后功能时你会感谢自己。3.4 后台管理模块与文件上传后台管理是“管理系统”这半边天的重点。功能上主要有商品管理、分类管理、订单管理、用户管理、数据统计。商品管理里最麻烦的是图片上传。开发阶段先在application.yml里配置本地上传路径file: upload-dir: /data/fishing-mall/upload/ access-path: /upload/**然后写一个上传文件的ControllerPostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext StrUtil.subAfter(originalFilename, ., true); String newFileName IdUtil.simpleUUID() . ext; File dest new File(uploadDir, newFileName); file.transferTo(dest); return Result.success(/upload/ newFileName); }再把WebMvcConfig里加一个静态资源映射让/upload/**的请求能找到本地真实文件registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir);上传后的图片URL直接存到商品表的main_image字段里前端拿到URL就能渲染。订单管理就是查询订单列表按状态筛选管理员对“待发货”的订单执行发货操作更新发货时间和状态。数据统计的话后台首页一般要展示几个核心指标用户总数、商品总数、今日订单数、今日销售额。用SQL聚合一下就能查到SELECT COUNT(*) FROM user WHERE role 0; SELECT COUNT(*) FROM goods WHERE deleted 0; SELECT COUNT(*) FROM orders WHERE DATE(create_time) CURDATE() AND status 2; SELECT IFNULL(SUM(pay_amount), 0) FROM orders WHERE DATE(create_time) CURDATE();4. 前端Vue实现打造体验顺滑的商城页面4.1 环境搭建与工程初始化前端这块Vue 3 Vite是目前最舒服的开发组合。用Vite创建项目特别快npm create vitelatest fishing-mall-web -- --template vue cd fishing-mall-web npm install然后安装核心依赖npm install vue-router4 pinia axios element-plus element-plus/icons-vueElement Plus的引入有两种方式全量引入和按需引入。全量引入适合项目小、图省事的情况直接在main.js里app.use(ElementPlus)就完事。但如果你对打包体积有要求或者面试想秀一手强烈建议用按需自动导入。安装两个插件npm install -D unplugin-auto-import unplugin-vue-components然后在vite.config.js里配置import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ], resolve: { alias: { : /src } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这里的server.proxy配置是前后端联调的关键。前端开发服务器跑在5173端口后端接口跑在8080端口跨域是必然的。配置代理后前端请求/api/user/login会自动转发到http://localhost:8080/api/user/login浏览器看到的请求同源跨域问题在开发阶段直接规避。关于按需导入我要特别提一个经典坑很多同学按网上的教程配了按需导入发现组件能用但ElMessage、ElMessageBox这种函数式组件一调用就提示ElMessage is not defined页面直接白屏。原因很简单——ElMessage是函数不是组件按需导入插件只自动引入了组件不会自动引入函数。解决办法是在main.js里手动导入样式或者用下面这种明确导入的方式import { ElMessage } from element-plus import element-plus/es/components/message/style/css ElMessage.success(操作成功)如果你不想在每一个页面都重复import可以在main.js里把它挂到全局app.config.globalProperties.$message ElMessage然后在组件里用this.$message访问组合式API里用getCurrentInstance()取。不过说实话这种全局挂载在Vue 3里不算最优雅更主流的方式是封装一个工具文件在需要的地方显式导入。4.2 前端目录结构与商城页面拆解前端工程的目录结构我习惯这样组织src ├── api // 接口请求封装 │ ├── user.js │ ├── goods.js │ ├── cart.js │ └── order.js ├── assets // 静态资源 ├── components // 公共组件 │ ├── NavBar.vue │ ├── GoodsCard.vue │ └── Pagination.vue ├── router // 路由配置 │ └── index.js ├── store // Pinia状态 │ ├── user.js │ └── cart.js ├── views // 页面组件 │ ├── home │ ├── goods │ ├── cart │ ├── order │ ├── user │ └── admin ├── utils // 工具 │ ├── request.js // axios封装 │ └── auth.js ├── App.vue └── main.js商城前台页面核心就那么几个首页轮播图 热门商品推荐 分类快捷入口。商品列表页左边分类树中间商品卡片列表顶部搜索框。配合分页组件。商品详情页展示商品主图、价格、销量、规格参数、详情图文点击“加入购物车”或“立即购买”。购物车页勾选商品、修改数量、删除、全选底部实时计算选中商品总价。结算页填写收货地址选择配送方式提交订单。订单列表页展示订单状态待付款的可以模拟支付待收货的可以确认收货。登录/注册页账号密码登录注册后自动登录。后台管理页商品管理表格、分类管理、订单管理通常做成一个侧边栏布局用router-view切换子页面。首页和商品列表页的数据直接调后端的公开接口。比如在src/api/goods.js里import request from /utils/request export function getGoodsPage(params) { return request({ url: /api/goods/page, method: get, params }) }axios封装是前端工程质量的关键一环。统一处理baseURL、超时时间、请求头、错误码// utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /, 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 { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request4.3 商品筛选与购物车交互细节商品筛选这块前端要处理“分类选中状态”和“搜索关键字”这两个查询条件。我建议直接用路由参数来驱动也就是把categoryId和keyword放到URL的query里比如/goods?categoryId3keyword路亚。这样做好处非常明显详情页跳回列表页时条件还在刷新页面时条件不丢失用户还能直接分享一个带筛选条件的链接。组件里用watch监听路由变化变化时重新拉取数据import { useRoute } from vue-router import { ref, watch } from vue const route useRoute() const goodsList ref([]) const total ref(0) const loadGoods async () { const params { pageNum: currentPage.value, pageSize: 12, categoryId: route.query.categoryId || undefined, keyword: route.query.keyword || undefined } const res await getGoodsPage(params) goodsList.value res.data.records total.value res.data.total } watch(() route.query, loadGoods, { immediate: true })购物车页的面积相对复杂因为涉及到“选中状态”和“总价计算”。我推荐把购物车数据存储在Pinia里页面组件只负责渲染和派发action。核心的state结构大致如下// store/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [], // 购物车商品列表 checkedIds: [] // 勾选的商品id集合 }), getters: { checkedItems: (state) state.items.filter(item state.checkedIds.includes(item.id)), totalPrice: (state) { const checked state.items.filter(item state.checkedIds.includes(item.id)) return checked.reduce((sum, item) sum item.price * item.count, 0).toFixed(2) }, cartCount: (state) state.items.length }, actions: { async fetchCart() { const res await getCartList() this.items res.data this.checkedIds this.items.map(item item.id) }, async removeItem(id) { await removeCartItem(id) this.items this.items.filter(item item.id ! id) this.checkedIds this.checkedIds.filter(checkedId checkedId ! id) } } })“立即购买”和“从购物车结算”这两条购买路径前端走到最后都是跳转到同一个结算页。下单接口收到的参数统一是“商品ID数量”的数组。区别在于购物车结算前会把对应的购物车记录查出来传过去立即购买则用当前商品构造一条数据。下单成功后的回调前端要做的第一件事就是清空本地购物车状态然后跳转到“我的订单”页面。4.4 Vue 3选项式与组合式API的取舍Vue 3发布后很多人纠结写选项式还是组合式。我的建议很明确新项目直接写组合式script setup原因很简单——组合式API把“同一业务逻辑”的代码聚在一起而不是按data、methods、computed这些选项强行分段。比如一个商品列表页跟筛选相关的状态、请求方法、监听器、计算属性在组合式API里可以写在一块阅读和维护都轻松。举一个简单的对比选项式的写法export default { data() { return { goodsList: [], loading: false } }, methods: { async loadGoods() { this.loading true const res await getGoodsPage() this.goodsList res.data.records this.loading false } }, mounted() { this.loadGoods() } }组合式的写法script setup import { ref, onMounted } from vue import { getGoodsPage } from /api/goods const goodsList ref([]) const loading ref(false) const loadGoods async () { loading.value true try { const res await getGoodsPage() goodsList.value res.data.records } finally { loading.value false } } onMounted(loadGoods) /script两者实现的功能一样但组合式写法把相关逻辑聚合成一个loadGoods函数一旦页面上有多个业务模块比如“商品列表、分类筛选、购物车角标”它们之间的代码分组会清晰很多。面试时如果被问到“选项式和组合式区别”核心要答到两点一是逻辑复用的方式组合式可以用自定义hook选项式依赖mixin二是代码组织方式按选项划分 vs 按业务逻辑划分。5. 前后端联调、部署上线与高频问题排查5.1 跨域问题开发与生产阶段的两种解法跨域是前后端分离项目里第一个“见面礼”。开发阶段用Vite的proxy代理解决就行前面已经展示过。这里再强调一下proxy只对开发服务器生效打包成静态文件部署在Nginx上时浏览器依然会面临跨域。生产环境的解决方案是在Nginx里配置反向代理把前端静态资源和后端接口放在同一个域名下server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/fishing-mall; index index.html; try_files $uri $uri/ /index.html; # 关键解决history路由刷新404 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里最关键的是try_files $uri $uri/ /index.html。Vue Router如果用了history模式默认的createWebHistory打包后直接放在服务器上刷新页面时Nginx会去按路径找文件结果找不到就报404。加上这行配置所有找不到的路径都会回退到index.html由前端路由接管问题迎刃而解。当然如果你后端项目里配置了全局CORSCrossOrigin或WebMvcConfigurer里加CorsFilter那从浏览器直连接口也是可以的。但生产环境走Nginx反代是更规范、更安全的做法还方便统一做HTTPS证书、负载均衡。5.2 打包部署的完整流程部署这件事听起来是最后一步但非常考验实操能力。我给出一个前端后端分离部署的参考流程。后端打包mvn clean package -DskipTests打出来的jar包加上SpringBoot内嵌的Tomcat直接扔到服务器上java -jar fishing-mall-0.0.1.jar --server.port8080也可以用nohup让进程常驻后台nohup java -jar fishing-mall-0.0.1.jar logs/run.log 21 前端打包npm run build执行完会在项目根目录生成dist文件夹把里面的所有文件上传到服务器上放到/var/www/fishing-mall就行。整个部署架构就是Nginx托管静态页面 → 浏览器发起请求 → Nginx把/api开头的请求转发给Java进程 → Java处理业务返回JSON → 前端渲染。如果你只是想快速演示给老师看不想买服务器也可以前后端打成一个包。把前端dist目录里的文件复制到SpringBoot项目的src/main/resources/static目录下重新打包后端。这样访问http://localhost:8080就能直接看到前端页面接口走同源。这种方式省了Nginx但每次前端更新都要重新打一次后端包后续维护不太方便适合终期演示用。5.3 高频问题排查速查表我把这个项目开发过程中自己和身边同学遇到最多的问题整理了一下做成一个速查表方便你对照排查。现象可能原因解决方法前端访问接口报CORS错误开发环境没有配置proxy或生产环境Nginx没有反代vite.config.js里配置server.proxy生产用Nginx配置location /api登录接口能通业务接口401拦截器拦截了请求token没带或已过期检查axios请求拦截器是否设置Authorization头检查token过期时间数据库连接失败/时区报错URL缺少时区参数或驱动版本不匹配URL加?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8商品列表查出来字段全是null驼峰映射没开启application.yml配置map-underscore-to-camel-case: trueElMessage报未定义Element Plus按需导入没处理函数式组件手动import { ElMessage } from element-plus并引入对应样式刷新页面404Vue Router用了history模式服务器没配置回退Nginx加try_files $uri $uri/ /index.html下单成功后购物车还在下单成功后前端没有清空本地购物车状态下单成功回调里调用cart store的clear方法图片上传成功但前端不显示静态资源映射没配置WebMvcConfig里addResourceHandlers配置/upload/**映射到本地目录SpringBoot启动时端口被占用8080被其他进程占用换端口或者杀掉占用进程Windows上netstat -ano查PIDnpm install超时npm源在国外网络不稳定设置淘宝镜像npm config set registry https://registry.npmmirror.comMySQL 8连接报Public Key Retrieval异常MySQL 8的caching_sha2_password认证问题JDBC URL加allowPublicKeyRetrievaltrueuseSSLfalse这些问题里数据库时区和ElMessage未定义是最常见的差不多占了所有报错的三分之一。遇到问题别慌先看报错信息里的关键提示再对照表里的排查方向一步步走90%都能自己解决。5.4 上线前的安全与性能基础检查系统做完能跑不代表可以放心上线。几个基础的安全和性能检查做完了心里才有底。第一密码不要明文存库。至少要做到BCrypt加密或者用加盐MD5。如果数据库泄露了明文密码等于把所有用户的账号拱手送人。这点在问老师演示的时候是加分项在真实生产环境里是保命项。第二接口要做好参数校验。下单时商品数量不能是负数不能是超大数字购物车商品ID必须是真实存在的。可以用Validated注解配合NotNull、Min这些校验注解在进入Service层之前就拦掉非法参数。第三数据库连接池别用默认值。SpringBoot默认的HikariCP参数适合开发环境生产环境下建议显式配置最大连接数、连接超时时间。比如spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000第四后端接口不要在响应里返回日志敏感信息。异常处理类里对外的异常信息要友好但堆栈信息要记到日志文件里尤其是SQL异常有的会直接把表结构带出来。第五上传文件要做类型和大小校验。后端校验文件扩展名限制大小比如不超过5MB存储文件名用UUID重命名不要用原始文件名。这样能规避掉相当一部分上传漏洞。这些内容不复杂但能体现一个开发者的工程素养。答辩或者面试时主动说出这些点比你背十道八股文都管用。最后再分享一点我的体会做完这个渔具商城项目我个人最大的感受是一个系统的难点往往不在某个单一技术栈而在各个模块之间的衔接。前端Vue写得再花哨后端接口设计不合理联调时照样寸步难行后端业务逻辑再严谨前端状态管理一团乱麻用户操作时照样出现各种诡异bug。比如购物车的“勾选状态”看似是前端的小事但实际上如果Pinia里的checkedIds和后端购物车表没有对应关系结算时的“已选商品”就会出错。再比如订单状态前后端各写一套状态常量如果值对不上就会出现前端显示“待发货”而后端实际是“待付款”的尴尬局面。所以做这类系统时我强烈建议先把接口文档定好状态的枚举值、请求参数的字段名、响应的结构前后端统一约定后面能省掉80%的扯皮时间。后续如果你想在这个项目上继续扩展方向还挺多的可以对接真实的支付沙箱环境把模拟支付替换成真正的扫码支付可以给商品模块加一个库存预警功能低于阈值自动提醒店主补货也可以把Redis引入进来做热门商品的缓存、购物车的临时存储。渔具垂直电商这个领域虽然不算大但围绕钓鱼人群可以做会员体系、钓场预约、二手渔具交易每一个方向都是很好的练手机会。先把这套基础打牢后面怎么玩都有底气。
返回列表