ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue的华强北二手手机交易管理系统设计与实现

基于SpringBoot+Vue的华强北二手手机交易管理系统设计与实现 1. 华强北场景下的系统需求梳理为什么二手手机交易需要一套独立系统从题目本身说起。华强北商城二手手机管理系统本质上是一个面向二手手机交易的电商平台。和全新手机商城不同二手手机领域有几个绕不开的痛点同一个机型因为成色、版本、有无维修历史价格能差出好几倍买家看不到实物只能依赖卖家的描述和照片交易过程中涉及验机、议价、售后等一堆环节。所以这套系统在设计之初就不能简单套用普通电商模板必须围绕二手这两个字做文章。我当时选这个题目的原因很简单SpringBootVueMySQL是当前Java后端方向最主流的技术组合企业里用得多网上参考资料也多遇到问题容易排查。而二手手机这个业务口径相比做一个泛泛的商城系统更有辨识度答辩时也好讲业务。更重要的是这个题目的功能边界足够清晰——用户端看商品、下单、收藏管理端管用户、管商品、管订单一套标准角色权限模型下来前后端该练的技术点基本全覆盖了。系统的角色划分是典型的三种身份普通用户注册登录、浏览商品、搜索筛选、收藏、下单购买、查看个人订单。卖家角色这里有两种处理方式一种是独立注册成为商家另一种是普通用户也能发布商品。我当时做的是后者降低用户发布门槛更贴近C2C场景。管理员用户管理禁用/启用账号、商品管理下架违规或已售商品、订单管理查看全部订单流、公告管理。为什么这么设计因为二手交易的核心是个人对个人如果照搬B2C那套平台统一发货的逻辑反而增加了系统复杂度。让每个登录用户都能发布自己闲置的手机管理员做后台审核和治理这更符合华强北这种线下市场线上化的真实场景。答辩时这也是一个可以展开讲的业务分析点。2. 数据库设计9张核心表如何支撑完整交易闭环2.1 用户、商品、订单这三张表的设计思路数据库是整个系统的地基。我花了整整两天时间设计表结构期间推翻了三版。最后定下来的核心表是这9张用户表、手机商品表、商品图片表、订单表、订单详情表、收藏表、公告表、评论表、管理员表。用户表没什么好说的常规字段id、username、password、nickname、avatar、phone、role区分用户和管理员、status是否被封禁、create_time。密码存的是BCrypt加密后的密文不是明文这个习惯毕业设计里也要养成。手机商品表是重点设计时针对二手特性加了几个关键字段字段名类型说明titlevarchar(100)商品标题如iPhone 13 Pro 远峰蓝 256G 国行descriptiontext商品详细描述pricedecimal(10,2)售价二手手机价格浮动大用decimal避免浮点误差original_pricedecimal(10,2)原价/专柜价用于展示折扣力度condition_levelvarchar(20)成色等级如99新、95新、9成新brandvarchar(50)手机品牌华为/苹果/小米/OPPO等modelvarchar(50)具体型号storagevarchar(20)存储容量128G/256G/512Gstatusint商品状态1在售2已售3下架seller_idint发布者ID关联用户表view_countint浏览数可做热门排序注意这里没有用单一分类ID去关联分类表而是直接把brand、model、storage做成了独立字段。原因很简单二手手机有强筛选需求我要搜iPhone、512G、9成新以下这类条件用字段直查比联表高效得多。这也是我做毕设过程中一个很重要的体会表结构设计的核心不是追求范式完美而是让最常用的查询路径最短。订单表和订单详情表的拆分是最初版本没做好的地方。最早我把订单信息全塞进一张表结果用户一次买两台不同型号的手机订单记录就乱套了。后来改成订单主表和订单明细表的经典结构订单表存订单号、总金额、买家ID、下单时间、订单状态待付款/已付款/已完成/已取消订单详情表存订单ID、商品ID、单价、数量。这样每笔订单可以包含多个商品扩展性和可解释性都更强。2.2 收藏、公告、评论这些辅助表的功能定位收藏表很直观字段就三个id、user_id、goods_id、create_time联合唯一索引避免重复收藏。公告表用来发平台通知比如平台升级通知交易规则变更字段是title、content、create_time。评论表稍微特殊我设计成商品评论和卖家回复共用一张表通过reply_id字段区分reply_id为0表示一级评论买家评价reply_id不为0表示对某条评论的回复。这里有一个实践中踩过的坑收藏表和外键的关系。很多教程喜欢在表设计时加物理外键FOREIGN KEY看起来关系图很漂亮。但实际开发中物理外键会导致插入和删除操作都要校验关联数据性能损耗大项目后期想改结构也麻烦。我的做法是不建立物理外键只在业务层维护逻辑关联。这也符合现代企业级开发的主流实践答辩时如果老师问起来这个理由完全站得住脚。数据库的初始化脚本我用Navicat导出后又手工调整了一遍把所有表的存储引擎统一改成InnoDB字符集utf8mb4排序规则utf8mb4_general_ci。InnoDB支持事务对订单这种需要强一致性的业务是必须的utf8mb4比utf8多支持emoji和一些生僻字商品描述里经常有人发特殊符号这个细节别忽略。3. SpringBoot后端落地JWT认证、文件上传、订单事务的完整链路3.1 为什么选JWT做登录认证而不是Session毕设项目通常会有多端访问的需求浏览器网页、管理后台、甚至要演示手机端调试如果用传统的Session方案前端每次发请求都要带Cookie跨域情况下处理起来很麻烦。我选JWTJSON Web Token的核心原因是无状态后端不存登录态用户登录成功后返回一串加密的Token前端存储起来后续请求放进Header里的Authorization字段即可。具体实现分为三步第一步引入依赖。pom.xml中加入jjwt相关依赖io.jsonwebtoken:jjwt-api、jjwt-impl、jjwt-jackson。第二步写JWT工具类。核心方法就两个generateToken根据用户名和ID生成Token和parseToken解析Token获取用户信息。签名密钥用了一个固定的字符串设置过期时间为24小时过期后强制重新登录。public String generateToken(Integer userId, String username) { Date now new Date(); Date expiryDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); }第三步写拦截器或过滤器统一校验Token。我选择的是写了一个HandlerInterceptor在preHandle方法里从Header中取出Token解析失败则返回401状态码和提示信息解析成功则把用户ID放进request的attribute里传给controller使用。JWT的坑也有两个值得提醒。一个是密钥泄露问题代码里直接写死密钥虽然方便但一旦项目代码泄露任何人都能伪造Token。实际生产环境应该把密钥放到配置文件或环境变量中。另一个是登录失效问题JWT一旦签发在过期之前无法主动失效如果用户被封禁了他的Token依然有效。我的解决办法是在拦截器里额外查一次用户状态如果status为0封禁状态就拒绝访问。多一条查询换来做严格的控制这笔账划算。3.2 手机图片上传的本地存储方案商品发布必须有图片上传功能用户上传手机实拍图管理端也要能在后台维护商品图片。文件上传这个功能看起来不复杂但涉及三个层面的问题存储在哪里、怎么访问、怎么限制。本地存储方案的核心逻辑其实很简单接收MultipartFile生成一个新的文件名我用UUID原文件后缀名保存到项目目录下的upload文件夹然后把访问路径拼接好存进数据库。这里注意几个细节文件重命名直接用用户上传的文件名有两个风险一是文件名重复会互相覆盖二是有可能包含非法字符。用UUID重命名一劳永逸。后缀名白名单校验只允许.jpg、.jpeg、.png、.gif这几种格式防止用户上传exe或jsp等危险文件。要做两层校验前端input标签accept属性限制一次后端代码再校验一次前端限制只是用户体验后端校验才是安全底线。虚拟路径映射SpringBoot中配置一个WebMvcConfigurer把本地的upload目录映射成 /upload/** 的URL路径这样图片就能通过http://localhost:8080/upload/xxx.jpg直接访问不用额外配静态资源服务器。Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); }这一步的为什么要讲清楚SpringBoot默认的静态资源目录是classpath:/static/但上传的文件是动态生成的不属于项目打包内容所以必须通过外部路径映射来暴露访问入口。如果直接把文件写到static目录里打包部署后文件就丢了。3.3 下单事务库存扣减和订单创建的原子性问题订单功能是电商系统的核心也是最容易出Bug的地方。直接给两行关键代码然后解释我在毕设中处理事务的三个层次。第一层使用Transactional注解保证方法级的事务。用户下单时系统要完成两件事往订单表插入一条订单记录、把对应商品的状态改成已售。这两步必须同时成功或同时失败否则就会出现订单里有商品、但商品还是在售状态的脏数据。Transactional public Order createOrder(Integer userId, Integer goodsId) { Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 1) { throw new RuntimeException(商品不存在或已下架); } // 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setGoodsId(goodsId); order.setPrice(goods.getPrice()); order.setStatus(0); orderMapper.insert(order); // 更新商品状态 goods.setStatus(2); goodsMapper.updateById(goods); return order; }第二层业务层面的并发控制。Transactional只能保证事务还不能防止超卖。两个用户同时下单同一个商品理论上都通过了status 1的判断最后商品状态虽然更新成2但订单却生成了两笔。解决思路是在执行更新时加条件判断UPDATE goods SET status 2 WHERE id ? AND status 1如果影响行数为0说明商品已经被别人抢走直接抛出异常。这是乐观锁思想的一种简化实现放在毕设中讲很有加分效果。第三层事务失效的常见原因。这也是我排错时踩过的坑——在同一个类里调用带有Transactional的方法事务会失效。因为Spring的事务是基于AOP代理实现的同类内部调用不走代理注解就不起作用。解决方案要么把方法拆到不同的Service类里互相调用要么自己注入自身代理最省事的办法是直接在那个方法内部把事务逻辑写在调用入口处。订单号的生成也很讲究我当时的实现是时间戳yyyyMMddHHmmss格式 用户ID补零 随机数拼成一串20位的唯一订单号。单靠数据库自增ID做订单号有两个问题一是不安全会被竞争对手猜到订单量二是多表关联时不够直观。业务订单号的意义是让客服和用户对单时有明确的标识这个习惯越早养成越好。4. Vue前端实现登录拦截、组件化页面和接口联调4.1 路由守卫和axios拦截器的双重登录校验前端用Vue 2 Element UI Vue Router Axios组合这也是当前毕设项目使用率最高的前端组合。我做了两个层面的登录控制路由守卫控制页面访问权限和axios拦截器控制接口请求权限。路由守卫写起来很简单在router/index.js中加一个beforeEach钩子判断是否有tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requireAuth !token) { next(/login); } else { next(); } });需要登录才能访问的页面个人中心、购物车、订单列表、发布商品等在路由配置里加一个meta: { requireAuth: true }标记。这样做的意义在于不做拦截的话用户直接输入URL就能绕过登录页面访问内部页面。需要注意的是前端路由拦截只是用户体验优化并不能真正保护数据安全因为后端接口才是数据出入口所以后端JWT校验才是安全主体。axios拦截器负责给每个请求自动附上Token同时统一处理后端返回的401service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );一个非常常见的报错——跨域问题就是这么出现的前端跑在localhost:8080Vue默认端口后端跑在localhost:9090两个端口不同就产生了跨域。解决方式我是在后端加了全局CORS配置允许来自前端地址的请求。如果你用Nginx做代理那就没必要在代码里配置CORS直接把请求转发到后端端口就可以了这个后面部署部分会讲。4.2 商品列表的条件筛选和分页展示商品首页是用户看到的第一屏体验好坏直接决定项目的观感。我用Element UI的el-card组件包裹商品卡片每个卡片展示封面图、标题、价格、成色标签点击跳转到商品详情页。商品列表页做成了三个筛选维度品牌下拉框、成色下拉框、关键词搜索框配合el-pagination分页组件。关键点在于这三个筛选条件不是独立运作的而是组合筛选。比如用户选了华为99新列表就必须同时满足这两个条件。实现方式是在前端把筛选条件拼成查询参数传给后端后端动态拼接SQL实现多条件查询。select idselectGoodsList resultTypecom.example.entity.Goods SELECT * FROM goods where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR model LIKE CONCAT(%, #{keyword}, %)) /if if testbrand ! null and brand ! AND brand #{brand} /if if testconditionLevel ! null and conditionLevel ! AND condition_level #{conditionLevel} /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{limit} /select这是MyBatis动态SQL最常用的场景where标签会自动去掉多余的AND关键字if标签实现条件判断。配合PageHelper分页插件后端返回一个带total总数的分页结果对象前端就能精确控制每页显示8条或12条数据。4.3 富文本编辑器的选择与踩坑商品描述和公告发布我用了wangEditor这个国产富文本编辑器轻量、中文友好、和Element UI搭配没有冲突。但这里有个很大的坑富文本编辑器的HTML内容存入数据库后页面展示时样式会丢失。原因是Element UI的el-card内部有样式隔离编辑器生成的HTML引用了它自己的CSS类名这些类名没有打包进全局样式。解决办法是在全局样式文件里引入编辑器的样式并且在展示商品详情的组件里把内容通过v-html渲染出来后在父级容器上追加编辑器的样式类名。这个坑我排查了一个晚上最终在浏览器开发者工具里对比样式差异才定位到。如果你也遇到富文本渲染样式丢失先看一眼Element的scoped样式有没有阻断编辑器样式这个方向基本一次就能找到问题。5. 部署全流程从JDK环境配置到Nginx反向代理5.1 后端环境准备和启动细节很多同学在答辩前才慌慌张张部署结果不是数据库连不上就是端口起不来。部署这件事真应该从开发第一天就心里有数。我的部署顺序是这样的第一步确认JDK版本和Maven环境。后端用了SpringBoot 2.7.2版本官方要求JDK 8及以上我用的是JDK 1.8。如果机器上装的是JDK 11或17运行是没问题的但要注意pom里java.version标签的值要匹配。我之前试过用JDK 17跑一个编译版本为1.8的项目结果SSL握手报错排查半天发现是加密套件兼容性问题。第二步修改application.yml中的数据库连接和上传路径配置。数据库地址、用户名、密码这些不能写死最好用环境变量或配置文件占位符。上传路径我用的是System.getProperty(user.dir)这在IDEA里运行指向项目根目录在linux服务器上用java -jar启动时指向jar包所在目录天然适配不同部署场景。第三步启动项目。开发环境用IDEA直接跑主类测试环境用mvn clean package打成jar包然后nohup java -jar xxx.jar catalina.out 21 后台运行。注意检查防火墙有没有放开9090端口云服务器还要在安全组规则里放行。前端部署分两种情况如果只是在本机演示用npm run serve跑一个开发服务器足够如果要放到云服务器就先npm run build打包把dist目录里的静态文件通过Nginx代理出去。5.2 数据库导入的注意事项我提供SQL文件时特别提醒使用者注意一点先创建数据库再导入表结构和数据。很多人直接双击运行SQL文件结果因为数据库不存在而报错。正确操作是CREATE DATABASE IF NOT EXISTS huangqiangbei DEFAULT CHARACTER SET utf8mb4; USE huangqiangbei;然后导入表结构和初始数据。我提供的SQL文件里有几个基础账号管理员账号admin/admin123和两个测试用户账号方便评审老师快速体验前后台功能。5.3 Nginx反向代理配置参考这里给一份我用的Nginx配置作用是把前端打包后的静态页面托管起来同时把 /api 路径的请求转发到后端服务规避跨域问题。server { listen 80; server_name localhost; # 前端静态资源 location / { root /usr/share/nginx/html/hqb-front; index index.html index.htm; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://localhost:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 图片访问 location /upload/ { proxy_pass http://localhost:9090/upload/; } }这里有三个细节容易被忽略一是try_files那句前端路由用的是history模式刷新页面时Nginx必须把不存在的路径都rewrite到index.html否则新闻页面刷新就404二是proxy_pass的location前缀如果前端请求的是/api/goods/list后端接口是/goods/list那么proxy_pass http://localhost:9090/;末尾的斜杠会丢弃/api前缀直接转到后端根路径这个逻辑务必理解清楚不然会出现接口404三是文件上传路径也要代理不然用户上传的图片通过Nginx访问时会报404。6. 排错实录部署和开发过程中最常卡住的三个问题6.1 端口占用导致的后端启动失败这一个问题在毕设答辩现场几乎必现。SpringBoot默认端口是8080当时我的前端Vue开发服务器也占用8080后端一直报Port 8080 was already in use。解决方式有两个一是换后端端口在application.yml里改成9090二是在启动命令里临时指定端口java -jar my-project.jar --server.port9090如果你用的是8080被占用的场景建议直接在配置里修改然后用netstat -ano | findstr 8080Windows或lsof -i:8080Linux/Mac找到占用进程kill掉。这里的经验是开发时前端一个端口后端一个端口一定要从一开始就分开否则后面每周都会踩一次。6.2 数据库中文乱码和时区问题的双连坑数据库连接串里的编码和时区参数是我见过同学们最常忽略的设置。很多人的jdbcUrl长这样jdbc:mysql://localhost:3306/huangqiangbei这样配置大概率会踩两个坑中文乱码和日期时间相差8小时。正确写法应该是jdbc:mysql://localhost:3306/huangqiangbei?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8保证中文字符正常存取serverTimezoneAsia/Shanghai指定时区。MySQL 8.0以上的数据库如果不指定serverTimezone连接时会直接报The server time zone value...的错误。乱码问题如果连接串已经配好还是乱码再检查一下建表语句的字符集确认是utf8mb4。6.3 MyBatis的Mapper接口和XML映射文件路径不匹配这个问题用一句话概括就是Maven默认不会把src/main/java目录下的XML文件打包到classes目录。如果你的MyBatis映射XML文件跟Mapper接口放在同一个包下运行时会报Invalid bound statement (not found)错误但IDEA里启动却不报错。因为IDEA编译时会把XML文件一起复制过去而Maven打包时不会。解决方案是pom.xml里加上资源过滤配置build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build或者更推荐的做法把XML文件放到src/main/resources/mapper/目录下在application.yml里指定mapper-locations路径这样打包部署都没有隐患。7. 论文写作与项目答辩的衔接经验毕设项目做出来只是第一步论文和答辩才是真正决定成绩的那一关。我的经验是写论文时不要把网上的XX系统设计与实现模板拿来硬抄要结合自己项目里实际用到的技术点展开。我论文的核心章节对应关系是这样的系统需求分析章节画用例图和用例表把用户、卖家、管理员三种角色的操作流程写清楚这些来自实际编码时的业务梳理系统设计章节把数据库ER图和9张表结构放进去每张表加注释说明设计理由系统实现章节选3到5个有代表性的功能模块详细展开我选了JWT登录认证、商品条件筛选、下单事务处理、图片上传这四块每块都写清楚实现思路、核心代码、运行效果截图系统测试章节按功能模块列了二十多条测试用例每条包含测试步骤、预期结果、实际结果这个表是答辩评委最常翻阅的。写论文时最容易出问题的是截图。很多同学随便截几张图贴上去甚至截得模糊不清这是硬伤。每个功能模块建议截三张操作入口页面、填写数据后的中间状态、提交后的结果反馈页把整个业务流程的闭环展示出来。答辩PPT的讲解思路也有讲究。我的顺序是项目背景和意义一分钟讲完→ 系统功能演示用浏览器真实跑一遍流程这是重头戏占一半时间→ 技术亮点和难点讲JWT无状态认证、MyBatis动态SQL、订单并发控制三个点→ 总结和展望。全程控制在十分钟以内讲完留五分钟给评委提问。提问环节最常被问的问题无非这几个为什么选这个课题、数据库为什么这么设计、遇到过什么技术难点怎么解决的、项目还有哪些可以优化的地方。这些问题提前都能准备到心里有底就不慌。我用个人实际项目经历负责任地说一句话毕业设计这个环节完成比完美重要跑得通比写得花哨重要能讲清楚比功能多重要。把上面这条路走完从数据库设计到后端接口、从前端页面到部署上线每一步都亲手敲过一遍答辩时你自然就有底气。
返回列表