ARTICLE DETAIL

资讯详情

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

校园闲置物品交易系统:Spring Boot+Vue前后端分离实战

校园闲置物品交易系统:Spring Boot+Vue前后端分离实战 前后端分离的校园闲置物品交易系统Spring Boot Vue MyBatis MySQL这套组合最近在技术社区里讨论热度一直很高。说实话这类系统确实是前后端分离入门到进阶之间一个非常好的练习载体业务模型清晰、功能边界明确、涉及的技术点覆盖广又不会像电商中台那种大型系统一样让人无从下手。我自己前后带过几批新人做过类似项目也帮别人排查过不少这类系统的问题这次就把整套从设计到部署的完整思路和实操过程整理出来。这套系统的核心价值在于业务完整度高、技术栈主流、可扩展性强。对在校学生来说它可以直接作为课程设计或毕业设计的原型对刚入行的开发来说它是一个能写进简历、面试时能清楚讲明白业务逻辑的实战项目对想转前后端分离开发模式的人来说它是一套可以照着敲、跑起来看效果的完整范例。无论你是哪种情况这篇文章都会尽量把关键环节讲透。1. 整体设计与技术选型思路1.1 为什么选前后端分离架构前后端分离已经是目前Web开发的主流形态了。前几年做这类系统很多人还用JSP Servlet Bootstrap那一套前端页面直接嵌在后端工程里开发的时候耦合度高改个按钮样式都要重新部署后端。前后端分离的核心思路是前端只负责页面渲染和用户交互通过HTTP接口请求数据后端只负责业务逻辑和数据处理通过JSON格式返回结果。两者通过接口文档约定契约各自独立开发、独立部署。这套模式带来的直接好处有三个。第一开发和调试效率大幅提升。前端跑在Node环境里有热更新改代码立即生效后端跑在Spring Boot内置的Tomcat里用DevTools也能做到改完自动重启。两边可以同时开工不用互相等待。第二部署灵活。前端构建出来的是一堆静态文件扔到Nginx里就能跑也可以直接打进Spring Boot的resources目录后端可以独立部署在多台机器上用Nginx做反向代理后面要扩展到集群也容易。第三团队协作更顺畅。这个系统虽然一个人也能做完但如果你用前后端分离的结构去组织代码天然就有一种分工的边界感前端管components、views、router后端管controller、service、mapper。后面加人进来接手也容易。当然前后端分离不是没有代价。跨域问题、Token鉴权、接口联调沟通成本这些都是需要额外处理的。我在后面会逐个讲解。1.2 技术栈选型的取舍逻辑这套系统的技术栈是Spring Boot Vue MyBatis MySQL每一环都有它的理由不是拍脑袋选的。Spring Boot选它是因为它把Spring生态里大量繁琐的配置自动化了。以前用Spring MVC写一个项目要配web.xml、spring-mvc.xml、数据源、事务管理器一堆东西Spring Boot用starter机制加自动配置一个启动类就能把应用跑起来这对中小型项目和初学者来说极其友好。而且Spring Boot 2.x到现在3.x已经非常成熟社区资料丰富遇到问题基本都能搜到答案。Vue选它是因为它的学习曲线相对平缓模板语法直观而且配合Element UI这类组件库开发后台管理界面和中台页面非常快。React当然也能做但Vue在国内的生态和资料更接地气对中文开发者更友好。MyBatis选它是因为它对SQL的控制力很强。像校园闲置物品交易系统这类业务查询条件往往动态变化按分类筛选、按价格区间筛选、按关键词模糊搜索这些用MyBatis的动态SQL写起来非常灵活。相比之下如果业务查询没那么复杂用JPA也能省不少事但JPA在复杂查询和SQL调优方面不够直观。这里我的建议是以MyBatis为主复杂查询写XML简单CRUD用注解或者通用Mapper两者结合效率最高。MySQL作为存储层不用多说它是目前中小型项目最稳妥的选择事务支持完善、性能足够、运维成本低。为了演示方便后面建表脚本和数据初始化脚本我都会给出来你本地装一个MySQL 5.7或8.0都能直接跑。2. 核心功能模块与数据库设计2.1 功能模块划分做这类系统第一步不是急着写代码而是把功能模块理清楚。我习惯先把角色和用例画出来再反推数据库表结构。校园闲置物品交易系统核心角色有四个游客、买家、卖家、管理员。游客和登录用户看到的内容不一样买家和卖家在业务上实际是同一类用户在不同场景下的身份切换。功能上我从实际使用角度拆成四个模块。用户模块负责注册、登录、个人信息管理。注册时默认是普通用户管理员账号由数据库初始化脚本写入。登录用JWT签发Token前端把Token存在localStorage里每次请求带上后端用拦截器统一校验。商品模块是系统的核心包括商品发布、商品列表展示、商品详情、商品编辑、商品下架、商品搜索与筛选。商品状态我设计为枚举1表示在售、2表示已售出、3表示下架、0表示审核中。发布商品需要填写标题、描述、分类、价格、成色、图片等字段。交易模块负责下单、订单状态流转、买卖双方确认、订单删除。这里不涉及真实支付只做模拟交易流程买家下单后订单状态为待确认卖家确认后变为待收货买家确认收货后变为已完成整个过程可取消。管理模块给管理员使用负责用户管理、商品审核、分类管理、数据统计。用户发布商品后默认进入审核中状态管理员审核通过后才会在列表中展示这能有效过滤垃圾信息。2.2 数据库表结构设计数据库设计是这类系统最容易踩坑的地方。我见过不少人上来就建表后面改来改去代码和表结构拧在一起非常痛苦。这里我把核心表结构方案直接列出来并且会解释每个字段的用意。用户表是基础字段包含id、username、password、nickname、avatar、phone、email、role、status、create_time。password存储的是BCrypt加密后的密文不要存明文。role字段用int类型1为普通用户2为管理员这样扩展角色也方便。商品分类表id、name、sort、create_time。这个表内容简单但容易被人忽略。分类不要硬编码在前端因为分类是后续运营时要调整的存数据库里后端和前端都能动态读取。商品表字段比较多id、user_id、category_id、title、description、price、original_price、condition_level成色、images、status、view_count、create_time、update_time。这里几个设计细节值得说一下。price字段用DECIMAL(10, 2)不要用FLOAT或DOUBLE因为浮点数在比较和计算时会有精度问题。images字段我建议用逗号分隔的多个图片路径字符串比如/images/1.jpg,/images/2.jpg这样读取时用split方法拆一下就能拿到图片列表。如果图片数量可能很多再单独建一张商品图片表也行但对这个系统来说字符串方案已经完全够用而且查询时省了一次关联查询。订单表id、order_no、product_id、buyer_id、seller_id、price、status、create_time、pay_time、confirm_time。order_no用时间戳加随机数生成保证唯一性不要用自增id直接当订单号这样容易暴露业务数据量而且可能在并发时出问题。收藏表id、user_id、product_id、create_time。这个表加一个联合唯一索引uk_user_product(user_id, product_id)防止用户重复收藏同一条商品。留言表id、product_id、user_id、content、create_time。用于商品详情页的评论互动这个功能看起来不起眼但面试时能体现你考虑问题的完整性。把表关系理清楚之后可以用一个简单的例子串一遍流程用户A发布一台九成新的自行车状态为审核中管理员在后台看到审核请求审核通过后状态变为在售用户B在商品列表搜到自行车点进详情后下单A收到订单消息后确认交易B确认收货后订单完成商品状态变为已售出。这条链路涉及用户表、商品表、订单表的联动数据库表之间的外键关系虽然不强制在数据库层面建立但逻辑上一定要清晰。2.3 接口设计规范在写后端代码之前接口要先定好。我采用RESTful风格设计API统一返回格式为{ code: 200, message: success, data: {} }code为200表示成功非200表示业务异常或系统异常。这样前端axios拦截器里只需要判断code和HTTP状态码处理逻辑统一。接口按模块划分认证接口POST /api/auth/register、POST /api/auth/login用户接口GET /api/user/info、PUT /api/user/info商品接口GET /api/product/list、GET /api/product/detail/{id}、POST /api/product、PUT /api/product、DELETE /api/product/{id}订单接口POST /api/order、GET /api/order/list、PUT /api/order/{id}/status管理接口GET /api/admin/product/list、PUT /api/admin/product/audit、GET /api/admin/user/list接口路径的命名有几个讲究。第一统一加/api前缀后面接Nginx反代时方便区分静态资源和动态请求。第二用名词复数表示资源URL里不要出现动词比如用/api/product不是/api/getProduct。第三列表接口统一支持pageNum、pageSize、keyword、categoryId等查询参数后面做分页查询时能保持一致的代码模式。3. 后端Spring Boot核心实现3.1 项目初始化和基础配置创建Spring Boot工程最简单的方式是用Spring Initializr在IDEA里直接新建项目时选好依赖就行。这里建议手动加依赖版本可控遇到问题也好排查。pom.xml核心依赖是这样的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesSpring Boot 2.7.x是目前稳定性最好的版本线JDK 8和17都能跑。如果你非要上Spring Boot 3.x要注意javax包名改成jakarta部分配置写法也有变化第一次做项目没必要给自己增加这个复杂度。application.yml里重点配置三块数据源、MyBatis、文件上传。这里给一个参考配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_trade?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.campustrade.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置强烈建议开启这样数据库里的create_time能自动映射到Java实体类的createTime字段省去一大堆resultMap手动映射。数据源URL里必须加上serverTimezoneAsia/Shanghai否则MySQL 8.0驱动会报时区错误。useSSLfalse是避免本地开发时证书校验的麻烦生产环境再根据实际情况打开。3.2 JWT认证与登录拦截器用户认证是这类系统的安全基石我选择JWT方案。核心流程是用户登录成功后后端生成一个Token返回给前端前端把Token存起来每次请求在HTTP头里带上Authorization: Bearer token后端写一个拦截器拦截需要认证的接口从Token中解析出用户ID存入ThreadLocal供后续业务使用。JWT本身包含三部分Header、Payload、Signature。Payload里我存放userId和role还有过期时间。用java-jwt库来生成和解析Token代码比较简洁。public class JwtUtil { private static final String SECRET your-secret-key; public static String generateToken(Integer userId, Integer role) { Algorithm algorithm Algorithm.HMAC256(SECRET); Date expireDate new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000); return JWT.create() .withClaim(userId, userId) .withClaim(role, role) .withExpiresAt(expireDate) .sign(algorithm); } public static DecodedJWT verifyToken(String token) { Algorithm algorithm Algorithm.HMAC256(SECRET); return JWT.require(algorithm).build().verify(token); } }写拦截器的时候有个容易忽略的坑拦截器里解析Token失败时不能只返回一个错误JSON还需要把HTTP状态码设置为401否则前端axios拦截器判断逻辑会很别扭。另外放行路径要白名单化像登录、注册、商品列表、商品详情这些接口不需要认证直接放行。登录接口的实现逻辑是前端传用户名和密码过来后端用BCryptPasswordEncoder检查密文是否匹配匹配成功后生成Token返回。BCrypt比MD5安全得多MD5加盐虽然也能用但BCrypt是Spring Security的标准做法拿来即用。3.3 商品发布与图片上传商品发布是买家和卖家交互的起点。前端通过表单收集信息用multipart/form-data格式同时上传图片文件和商品数据。后端的Controller这样接收PostMapping(/api/product) public Result createProduct(RequestParam(file) MultipartFile file, RequestParam(title) String title, RequestParam(price) BigDecimal price) { String imageUrl fileService.upload(file); productService.createProduct(userId, title, price, imageUrl); return Result.success(); }图片上传的存储方案这里我推荐本地磁盘存储。在服务器上建一个/data/images目录文件按日期分目录存放比如/data/images/2025/06/01/xxx.jpg。文件名用UUID加时间戳生成防止重名。不要直接用用户上传的原始文件名一是可能有路径穿越风险二是重名概率高。上传代码的核心逻辑public String upload(MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!allowedExt.contains(ext.toLowerCase())) { throw new BusinessException(不支持的文件类型); } String fileName UUID.randomUUID().toString().replace(-, ) ext; String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); File dir new File(uploadPath / datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); return /images/ datePath / fileName; }然后通过一个WebMvcConfigurer把本地的/data/images目录映射到/images/**这个虚拟路径上前端就能直接通过http://localhost:8080/images/2025/06/01/xxx.jpg访问到图片了。实际生产环境更常见的做法是把图片传到阿里云OSS或者腾讯云COS走CDN加速减轻应用服务器的压力。但本地磁盘方案对学习和中小规模部署完全够用后面需要再升级也容易只是多一个适配层的问题。3.4 MyBatis动态SQL实现商品筛选商品列表页的筛选条件是这套系统里最具代表性的查询场景。用户可能按照分类、价格区间、关键词、商品状态等多个条件组合筛选。这种需求如果用JPA写条件拼接会比较痛苦但MyBatis的动态SQL就是为这个场景设计的。在ProductMapper.xml里我这样写查询语句select idselectProductList resultTypeProduct SELECT * FROM product where if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动去掉第一个多余的AND这个细节很关键。注意gt;和lt;是XML里的转义写法直接写和会导致XML解析报错。排序用create_time DESC把最新的商品放在前面这是商品列表展示的合理策略。分页这里我没有引入PageHelper插件而是手动算offset。这样做的理由有两个一是这个系统数据量不大手动分页处理起来简单直观二是减少外部依赖避免PageHelper版本和MyBatis版本不兼容的问题。当然如果你习惯PageHelper引入也是可以的它确实能让分页代码简洁很多。3.5 订单状态机设计订单模块是整个系统里最容易逻辑混乱的部分核心问题就是状态管理。订单状态我定义为0待确认买家下单等待卖家确认1待收货卖家确认等待买家收货2已完成买家确认收货3已取消交易终止不同角色能执行的操作不一样买家可以下单和取消订单仅限待确认状态卖家可以确认订单和取消订单同样仅限待确认状态买家可以确认收货。这些限制条件必须写进service层不能只靠前端按钮隐藏来控制。我在实现时用了一个Map来约束合法的状态流转private static final MapInteger, ListInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, Arrays.asList(1, 3)); // 待确认 - 待收货 / 已取消 TRANSITIONS.put(1, Arrays.asList(2)); // 待收货 - 已完成 }状态变更的方法统一走一个updateOrderStatus方法先校验当前状态是否在合法流转路径上不在就直接抛异常。这样能有效防止并发或恶意请求跳过中间状态直接操作订单。订单状态变更还有一个容易忽略的点买家确认收货后要同步把对应的商品状态改成已售出。这个逻辑我放在同一个事务里用Transactional注解保证原子性。如果分两步操作不加事务中途出错就会出现订单已完成但商品还在卖的数据不一致问题。4. 前端Vue核心实现4.1 工程结构和路由设计前端我用Vue 3 Vite Element Plus组合。相比Vue 2 Vue CLI这套组合的构建速度和开发体验都更好。工程目录结构建议这样组织src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── store/ # 状态管理Pinia ├── views/ # 页面视图 │ ├── home/ # 首页 │ ├── product/ # 商品相关页面 │ ├── order/ # 订单相关页面 │ ├── user/ # 个人中心 │ └── admin/ # 后台管理 └── utils/ # 工具函数路由配置里我用到了路由懒加载这个对首屏加载速度优化很有帮助。首页和商品列表这些核心页面可以静态引入用户中心、后台管理等低频页面用动态import。路由守卫是前端鉴权的关键。系统里有些页面必须登录后才能访问比如发布商品、订单列表、个人中心。在router.beforeEach里做校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这里有个细节前端路由守卫只是体验层面的限制真正防住非法请求的是后端的JWT拦截器。前端跳转登录页只是引导用户后端返回401才是真正的安全边界。两者缺一不可。4.2 axios封装与拦截器axios需要在实例级别统一处理请求头、响应拦截和错误处理。封装的核心代码import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, // 配合Vite proxy timeout: 10000 }) 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) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message) return Promise.reject(error) } )baseURL设置为/api、配合Vite的proxy代理转发到后端8080端口这样做的好处是在开发环境和生产环境之间切换时不用修改前端代码。生产的Nginx配置里做一个/api开头的请求反向代理到后端服务就可以了。4.3 商品列表与详情页实现商品列表页是用户进入系统的第一个主要界面设计目标是信息展示清晰、筛选操作方便。我用Element Plus的el-card组件做商品卡片每个卡片展示商品主图、标题、价格、成色。筛选区放在页面顶部包含分类下拉框、价格区间输入和搜索框。搜索和筛选的逻辑是监听筛选条件变化重新请求/api/product/list接口通过categoryId、minPrice、maxPrice、keyword参数传递给后端。为防用户连续点击导致的接口请求频繁我在前端做了简单的防抖处理输入搜索关键词时延迟300毫秒再发请求实际使用体验会舒服很多。商品详情页有一个重要细节图片预览。el-image组件自带preview-src-list属性可以实现点击图片放大预览的交互。详情页核心信息区展示价格、成色、卖家昵称、发布时间下方是收藏按钮和下单按钮。收藏按钮要与登录状态绑定未登录点击时提示跳转登录页。详情页的数据加载要考虑Loading状态。我的习惯是页面加载时先展示骨架屏数据返回后渲染内容。这样在高延迟网络环境下用户体验不会太差。4.4 发布商品表单的关键校验发布商品页的核心是表单校验。Element Plus的el-form配合rules配置能覆盖大部分校验场景。价格字段用type: number配合min和max限制范围图片上传用el-upload组件限制文件类型和大小。图片上传这里要特别注意el-upload默认走的是ajax上传上传成功后得到图片URL再把URL作为商品表单的一个字段提交。而不是把File对象放进JSON里提交JSON序列化File对象会得到空对象这个坑我见过不少次。const handleUploadSuccess (response) { form.imageUrls.push(response.data) }发布成功后跳转到商品详情页用户能看到自己刚发布的商品。但此时商品状态是审核中只有管理员在后台审核通过商品才会在列表页展示。这个流程在逻辑上模拟了真实交易平台的审核机制。5. 前后端联调与部署5.1 开发环境跨域配置开发阶段最常遇到的问题就是跨域。浏览器同源策略会拦截localhost:5173前端Vite向localhost:8080后端Spring Boot发起的请求。解决方案有两个层面我建议两层同时配。前端Vite的proxy配置在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })后端CORS配置在Spring Boot里写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }两层都配上开发时用前端proxy部署后靠Nginx转发生效。allowCredentials(true)配合allowedOriginPatterns(*)要注意如果用了CrossOrigin注解这两个配置不能同时用*不然会有冲突。5.2 前后端分离部署方案生产环境的部署方案我推荐前端编译成静态文件交给Nginx托管后端打jar包用systemd守护运行。前端构建npm run build构建产物在dist目录把dist目录下的文件上传到服务器的/usr/share/nginx/html或者自己指定的任意目录。Nginx的配置关键点在于location匹配规则server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost: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模式在做页面刷新时如果Nginx找不到对应的真实文件会返回404。加上这行配置后所有前端路由都会回退到index.html由前端路由接管。后端打包命令是mvn clean package -DskipTests生成的jar文件用java -jar启动。生产环境建议用systemd管理进程设置开机自启和崩溃自动重启。进程的启动参数里要留出合理的JVM堆内存比如-Xms256m -Xmx512m这个系统并发量不大堆内存512MB基本就够用。数据库在生产环境的初始化直接把建表脚本导入MySQL即可。密码这类敏感信息不要硬编码在application.yml里用环境变量注入的方式配置。Spring Boot里读取环境变量很方便spring: datasource: username: ${DB_USERNAME:root} password: ${DB_PASSWORD:root}服务器上的环境变量写在systemd服务文件里或者用export命令设置。这样即使代码仓库泄露数据库密码也不会暴露。5.3 常见错误与排查记录我整理了一份这套系统搭建和运行过程中最常遇到的问题清单每一条都是实际踩坑记录。问题现象可能原因排查与解决方式Spring Boot启动报数据库连接失败数据库服务未启动、密码错误、URL参数缺失检查MySQL连接mysql -uroot -p确认驱动版本与数据库版本匹配确认URL带serverTimezone参数Vue页面请求接口404后端接口路径与前端不一致、Nginx未正确代理先用Postman直接请求后端接口确认可用再看浏览器Network里的实际请求URL和代理配置是否吻合前端提示跨域开发环境代理未生效、后端CORS未配置检查Vite proxy是否监听正确的端口确认后端CORS配置类被扫描到图片上传后访问404虚拟路径映射未配置、图片目录不存在确认WebMvcConfigurer的addResourceHandlers路径配置确认磁盘上目录确实存在Token过期后接口报401前端未正确处理401检查响应拦截器里是否有401的统一处理逻辑跳转登录页并清除本地缓存商品列表接口响应慢SQL未走索引、查询了不必要的关联表用EXPLAIN分析SQL执行计划在where条件上建立合适的索引检查是否懒加载了不需要的字段订单状态显示异常状态流转逻辑没做合法校验检查service层的状态机校验是否完善确认前端下拉框选项与后端状态值一一对应MySQL索引这块我再多说一句商品表的category_id、status、create_time这三个字段是查询高频字段建议建立联合索引ALTER TABLE product ADD INDEX idx_category_status_time (category_id, status, create_time);这个索引覆盖了列表页最常用的筛选条件组合。不过要注意索引不是越多越好每个索引都会拖慢写入速度所以只给高频查询加索引就够了。5.4 并发与安全需要关注的细节这个系统虽然校园场景并发量不大但作为项目实践提前考虑几个安全细节面试时也能加分。第一个是SQL注入防护。MyBatis的#{}是预编译方式能有效防止SQL注入但${}是字符串拼接存在注入风险。建议在项目里全局搜索一下${}的使用位置除了表名、排序字段等必须动态拼接的场景外一律改用#{}。第二个是密码安全。用户密码存的是BCrypt加密后的密文。BCrypt的机制是自动加盐即使两个用户密码相同加密后的结果也不同能有效抵抗彩虹表攻击。登录验证用matches方法public boolean checkPassword(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); }第三个是文件上传安全。除了限制文件类型和大小还要注意校验文件内容。比如用户传一个改名成.jpg的恶意文件文件头可能根本不是图片。简单的做法是用Java的ImageIO读取图片读取失败就拒绝上传try { BufferedImage image ImageIO.read(file.getInputStream()); if (image null) { throw new BusinessException(文件内容不是有效图片); } } catch (IOException e) { throw new BusinessException(文件内容不是有效图片); }第四个是接口的幂等性问题主要是下单接口。如果用户快速点击两次下单按钮可能创建两个重复订单。解决方案是前端按钮loading状态防重复点击后端再做一个简单的幂等校验用户和商品组合不能重复创建待确认的订单。// 在事务里校验 Order existing orderMapper.selectByBuyerAndProduct(buyerId, productId, 0); if (existing ! null) { throw new BusinessException(您已下单请勿重复操作); }5.5 如何把这个项目讲成面试亮点项目做完之后展示环节同样重要。很多人在简历里写开发了校园闲置物品交易系统但面试时讲不清楚技术亮点因为只是照着教程敲了一遍。这里我分享几个让项目更有说服力的切入点。第一能清楚讲出技术选型的理由。比如为什么用MyBatis而不是JPA为什么用JWT而不是Session每个决策都要有逻辑支撑。面试官问你为什么用JWT如果你回答大家都在用这场对话基本就结束了。正确的回答思路是这个系统前后端分离Session方案需要处理跨域Cookie问题JWT无状态、Token放在请求头里适配前后端分离更方便。再补充一点JWT的缺点比如无法主动失效配合黑名单或Redis可以解决能体现出你的思考深度。第二能画出项目架构图和数据表关系图。面试时手绘简单的架构图或者ER图是非常加分的能力。核心表之间的关系要能说得清清楚楚用户表与商品表是一对多用户表与订单表是多对多一个用户既可以是买家也可以是卖家商品表与收藏表是一对多。第三能针对某个功能做更深入的探讨。比如这个系统如果要支持搜索功能你会怎么做你可以回答当前是MySQL LIKE查询数据量大时可以用MySQL全文索引或者引入Elasticsearch做全文检索。再比如如果要接入真实支付你怎么设计你可以谈谈支付回调、订单状态同步、对账机制。这些问题没有标准答案但能展示你对系统演进方向的理解。我把这个项目相关的一些扩展方向列在下面你在简历上写项目展望时可以参考扩展方向需要解决的技术问题涉及的新技术点真实支付支付回调、订单超时关闭、对账微信支付/支付宝SDK、延迟队列消息通知买家下单通知卖家、卖家确认通知买家WebSocket、SSE、消息队列搜索优化商品搜索的准确性和性能Elasticsearch、IK分词、索引设计图片存储海量图片的存储和访问性能对象存储OSS/COS、CDN、水印处理高可用部署单点故障、水平扩展Nginx负载均衡、Redis缓存、主从复制我个人在做这类系统的过程中最深的一个体会是业务逻辑清晰比技术栈华丽更重要。很多人在做项目时喜欢堆新技术Redis、MQ、微服务全套往上加结果系统复杂度和工作量都上去了但核心业务逻辑反而没做好。校园闲置物品交易系统这个题目看着简单但真正把用户认证、商品流转、订单状态机这些基础业务做扎实做到每个环节都有理有据、每个异常都有兜底方案这本身就是一种很扎实的工程能力。最后再分享一个小技巧调试接口时不要光看浏览器报错多抓抓Network面板里的实际请求和响应。很多前端页面显示不正常的问题其实后端接口返回的数据已经是对的了只是前端解析或渲染的环节出了问题。养成先看Network再改代码的习惯能省掉大量无效的调试时间。如果大概率你是照着这篇文章一步步走下来的这套系统跑起来之后建议你花点时间把订单状态流转的边界情况再测一遍比如买家下单后卖家两天不处理、买家取消订单、卖家在待收货状态不能直接改价格——这些看起来不起眼的细节恰恰是面试官最喜欢深挖的地方。
返回列表