每年六月底的毕业季,宿舍楼下都会堆满教材、台灯、自行车和收纳箱,这些九成新的东西最后大多论斤卖给了收废品师傅;到了九月份新生入学,又有人花几十上百元买回同款教材。这两拨人之间的信息鸿沟,就是校园二手交易平台存在的意义。最近我把这套Spring Boot + Vue实现的校园二手交易平台完整整理了一遍,前后端分离架构,附带完整源代码和万字详设文档。这篇文章就从项目定位、技术选型、数据库设计、核心功能实现到部署运维,把整个项目拆开讲清楚。不管是拿来做毕业设计、课程设计,还是想真正在校内跑一套二手交易系统,都有直接可抄的参考价值。
先说项目能做什么。用户端支持注册登录、商品发布、图片上传、分类浏览、关键字搜索、商品详情、收藏、下单、个人中心管理;管理端支持用户审核、商品审核、分类维护、订单管理、公告管理和基础数据统计。整个系统覆盖了二手交易的核心闭环:发布-曝光-沟通-交易-售后。技术上用的是Spring Boot提供RESTful接口,Vue负责页面渲染和交互,数据通过JSON交互,前后端完全解耦。
1. 校园二手交易平台的真实痛点与功能边界
1.1 为什么校园闲置交易不能直接套用闲鱼方案
我在设计这套系统之前,先对着校园场景盘了一遍需求。很多同学第一反应是"直接用闲鱼不就行了吗",但真放在校园环境里,这几个问题绕不开:
第一,信任半径不同。闲鱼的交易双方是陌生人,靠芝麻信用和平台担保;校园里面的买卖双方大概率是同一所学校甚至同一个校区的学生,交易核心是"确认这个人确实是本校学生",而不是泛化的信用体系。所以系统里要有学生认证的环节,管理端能审核用户身份,这比复杂的信用模型实在。
第二,交易方式差异明显。校园二手交易大多是线下当面交易:约在食堂门口、宿舍楼下,一手交钱一手交货。这意味着订单状态设计不能完全照搬电商的"待付款-待发货-待收货-待评价"那套,更合理的状态是"待面交-已完成-已取消",物流环节可以整个去掉。
第三,商品品类的季节性特别强。开学季教材需求集中爆发,毕业季宿舍清仓量大;平时则是小家电、数码配件、体育用品零零散散。分类设计上要预留足够灵活的调整空间,管理端必须有分类管理功能,不能把分类写死在代码里。
第四,商品展示的本地属性。多数学生不会花心思写长描述、拍专业照片,所以发布表单要尽量简化,图片支持多张上传,描述给个200字以内就够,重点把价格、成色、交易地点这几个字段做得明显。
1.2 系统角色与功能模块划分
基于上面的分析,这套系统把用户分成三类角色:
| 角色 | 核心权限 | 主要页面 |
|---|---|---|
| 普通用户 | 发布商品、浏览搜索、收藏、下单购买、个人中心 | 首页、商品列表、商品详情、发布页、订单页、个人中心 |
| 平台管理员 | 用户管理、商品审核、分类管理、订单管理、公告管理 | 后台工作台、用户列表、商品审核列表、分类管理页 |
| 超级管理员 | 管理员账号管理、数据权限配置 | 系统管理相关页面 |
普通用户走前台,管理员走独立后台,前后台在同一个工程里通过路由和权限控制做隔离。权限控制这块用最简单可靠的方案:后端接口基于JWT解析用户身份和角色,前端路由配置守卫判断登录状态和角色,双端校验,避免"前端隐藏菜单但接口照样能调"的常见漏洞。
功能边界上我刻意做了一些减法。比如不做即时聊天,买家和卖家之间通过平台留言或订单备注交换联系方式;不做在线支付,面交场景下支付动作发生在站外;不做复杂推荐算法,商品排序用发布时间和热度两个基础维度。减法的逻辑很简单:校园二手场景的核心是信息匹配,不是交易担保,把不必要的功能砍掉,系统才能稳定跑起来、代码才能真正被读透。
2. 技术选型逻辑:Spring Boot 3 + Vue 3 前后端分离的依据
2.1 后端选择Spring Boot的核心原因
后端框架我选了Spring Boot而不是SSH或者纯Servlet写法,理由很直接:Spring Boot解决了Java后端开发里最烦人的配置问题。
传统SSM框架要手动配置web.xml、applicationContext.xml、spring-mvc.xml,光是让一个项目跑起来就要花半天时间;Spring Boot通过自动配置和starter机制,把大部分重复性配置直接封装掉。以本项目的依赖为例,引入spring-boot-starter-web就搞定了内嵌Tomcat、Spring MVC、JSON序列化三大件,引入mybatis-plus-boot-starter就搞定了数据库访问层。对于这种中小型管理系统,Spring Boot的开发效率比SSM高出一倍不止。
选择Spring Boot还有一个实际考量:内置Tomcat让部署变得极其简单。项目打包成可执行的jar包,服务器上只要装了JDK就能直接java -jar运行,不需要单独安装配置Tomcat。这在后面前后端分离项目的部署环节会体现得非常明显,我们照样能把前端dist目录放进去一起跑,也能拆出来单独部署,非常灵活。
Spring Boot 3.x是当前的主流版本,基于JDK 17,内嵌Tomcat 10,整体性能和新特性支持都更好。如果考虑兼容性和周边生态(比如某些老版本的MyBatis-Plus依赖适配问题),选2.7.x也完全够用。项目里核心差异不大,无非是javax包名改成jakarta这类细节。
2.2 前端为什么用Vue以及版本取舍
前端部分用Vue是现阶段做中小型管理系统的默契选择。Vue的渐进式设计让它上手门槛明显低于React,一个懂HTML和JavaScript的同学花一周时间就能写页面;同时组件化机制保证了中后台页面这种"表格+表单+弹窗"的高重复度界面能高度复用。
版本选择上我推荐Vue 3 + Vite + Element Plus的组合。Vue 3的Composition API在逻辑复用上比Vue 2的Options API清晰得多,以商品列表页为例,加载状态、分页参数、搜索条件、列表数据这些响应式状态可以独立维护,不用把所有方法都堆在methods里。Vite的冷启动速度和热更新体验相比webpack是代际提升,npm run dev几乎是秒开。Element Plus是Element UI的Vue 3版本,表格、表单、弹窗、分页这些后台常用组件都有现成的,而且样式比老版本更清爽。
有个容易被忽略的问题是前端node_modules的安装。Vue 3 + Vite的依赖树比较大,网络不好时npm install容易失败。我的实操建议是优先用npm官方源,装不上就换国内镜像源,具体命令:npm config set registry https://registry.npmmirror.com。还有Vite对Node版本有要求,Vite 4以上需要Node 14.18+,Vite 5需要Node 18+,开发前先node -v确认一下,不然跑起来就是各种莫名其妙的报错。
2.3 前后端分离架构的收益和数据交互约定
前后端分离不是赶时髦,它解决的是实际协作和维护问题。前端只负责渲染和交互,后端只负责提供数据接口,两边可以并行开发,只要接口约定提前定好。在校园二手平台这个项目里,我定义了几条核心约定:所有接口前缀为/api;返回结构统一为{code, message, data};日期时间统一用时间戳或格式化字符串传输,避免时区混乱;分页参数统一用pageNum和pageSize。
这套约定的价值在联调阶段才会真正体现。前后端同学照着同一个接口文档走,前端Mock数据跑通页面,后端用Swagger或Postman自测接口,最后联调时把前端的Mock地址切换到真实地址即可。我在文档里专门把每个接口的请求方式、参数、返回示例都列清楚了,就是为了减少联调阶段的返工。
3. 从商品发布到订单完成:数据库设计与状态流转
3.1 用户、商品、订单三大核心表的设计要点
数据库设计是整个项目的根基,表结构设计不合理,后面写代码全是补丁。我按业务实体把表拆成七张:用户表、商品表、订单表、分类表、收藏表、评论表、公告表。其中用户表、商品表、订单表是绝对核心,我把关键字段和设计意图列一下。
用户表核心字段:id、username、password(加密存储)、nickname、avatar、role(0普通用户/1管理员)、student_no(学号)、status(0禁用/1正常)、create_time。密码加密我用的BCrypt,这是Spring Security自带的加密算法,每次加密结果带随机盐,就算数据库泄露也不能直接反推出明文密码。学号字段是学生认证的关键,管理员在后台核对学号信息完成认证。
商品表核心字段:id、user_id(发布者)、category_id、title、description、price(用DECIMAL类型,避免浮点精度问题)、images(多个图片URL用逗号拼接)、degree(成色:全新/几乎全新/轻微使用痕迹/明显使用痕迹)、status(0待审核/1在售/2已下架/3已售出)、view_count、create_time。这里有个实战细节:images字段我用逗号分隔存储单条记录,虽然不够范式化,但省去了建图片子表的复杂度,查询时直接split就能拿到图片列表,对图片数量固定且不多(1-5张)的场景足够用。
订单表核心字段:id、order_no(订单编号)、product_id、buyer_id、seller_id、price、contact_info(联系方式)、deal_location(交易地点)、status(0待面交/1已完成/2已取消)、create_time、finish_time。设计要点是同时存了买家和卖家ID,查询"我买到的"和"我卖出的"都不需要关联查询商品表再来判断身份,直接查两个字段就行。order_no用时间戳加随机数生成,保证唯一性。
剩下的表里,分类表设计成两级结构(parent_id字段指向父分类),收藏表和评论表都以商品ID作为外键逻辑关联。
3.2 商品状态与订单状态的状态机设计
状态机是交易类系统的灵魂,状态设计得好,业务代码写起来就是几个if判断的事情;设计得乱,到处都是互相矛盾的状态赋值。商品状态我定义了四个:0待审核、1在售、2已下架、3已售出。状态流转只有几条固定路径:发布时进入待审核;管理员审核通过后进入在售;在售状态下用户可手动下架变为已下架;已下架商品重新上架则回到在售;订单创建后商品变为已售出。
订单状态对应三条路径:用户提交订单后进入待面交;买家确认收货或者双方线下交易完成并在平台确认后变为已完成;在待面交状态下双方任意一方取消则变为已取消。这里有个容易踩坑的地方:商品状态和订单状态必须联动。下单成功时商品要同步变成已售出,否则会出现"商品已经卖了还在在售列表里"的脏数据。这个联动我放在服务层用@Transactional注解包裹,保证两个状态的更新要么都成功要么都回滚。
3.3 事务处理在交易流程中的落地
以用户提交订单为例,代码逻辑看起来只是插入一条订单记录,实际上牵扯到三步操作:校验商品状态(必须是1在售)、插入订单记录、修改商品状态为已售出。这三步必须在一个事务里完成。
@Service public class OrderServiceImpl implements OrderService { @Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateVO vo, Long buyerId) { // 1. 校验商品状态 Product product = productMapper.selectById(vo.getProductId()); if (product == null || product.getStatus() != ProductStatus.ON_SALE) { throw new BusinessException("商品不存在或已下架"); } // 2. 插入订单记录 Order order = new Order(); order.setOrderNo(OrderNoGenerator.generate()); order.setProductId(product.getId()); order.setBuyerId(buyerId); order.setSellerId(product.getUserId()); order.setPrice(product.getPrice()); // ... 其他字段 orderMapper.insert(order); // 3. 修改商品状态为已售出 productMapper.updateStatus(product.getId(), ProductStatus.SOLD); return order.getId(); } }注意@Transactional注解默认只在RuntimeException时回滚,所以rollbackFor要显式设为Exception.class,否则遇到受检异常时事务不会回滚,数据库里就会留下一半数据。这个坑我在早期写SSM项目时踩过一次,后来所有涉及多表更新的方法都统一写成rollbackFor = Exception.class。
4. 后端核心实现细节:鉴权、文件上传与分页查询
4.1 JWT登录鉴权与拦截器配置
登录鉴权我用JWT + 拦截器的方式实现,没有引入Spring Security全家桶,原因是对这种中小型系统来说,Spring Security的过滤器链配置复杂度远大于它带来的安全收益,JWT本身已经够用。
流程是这样的:用户登录成功后,服务端生成一个包含用户ID和角色的JWT令牌返回给前端。JWT的payload是三段式的Header.Payload.Signature结构,服务端用秘钥对Payload签名,客户端拿到Token后每次请求放在请求头里,服务端拦截器校验签名和有效期即可。关键代码逻辑:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册等白名单接口 if (handler instanceof HandlerMethod == false) { return true; } String path = request.getRequestURI(); if (path.contains("/api/user/login") || path.contains("/api/user/register")) { return true; } // 从请求头中获取Token并校验 String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && JwtUtils.verify(token)) { // 把用户信息放入Request作用域,供Controller直接使用 request.setAttribute("userId", JwtUtils.getUserId(token)); return true; } // 校验失败返回401 response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(new Result<>(401, "登录状态已失效,请重新登录"))); return false; } }拦截器配置好之后,还有一个容易忽略的细节:前端传Token时习惯加上Bearer前缀,比如Authorization: Bearer eyJxxx。如果后端判断逻辑没处理这个前缀,就会导致登录状态一直校验失败。我统一在拦截器里做了replace("Bearer ", "")的兼容处理,这算是一个典型的联调经验。
4.2 图片上传方案:本地存储与MinIO对象存储的取舍
商品图片上传是这类平台的刚需。实现方案有两种主流选择,我在文档里都写了。
方案一是本地磁盘存储:图片保存到服务器的某个目录,通过Spring Boot的静态资源映射对外提供访问。配置非常简单:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB web: resources: static-locations: file:/home/project/upload/,classpath:/static/上传接口的核心逻辑是把MultipartFile写入目标目录,然后返回可访问的URL。实测下来这个方案在单机部署、开发学习阶段完全够用,缺点是图片不便于迁移和备份,多机部署时图片不在同一台服务器上就没法访问。
方案二是引入MinIO对象存储。MinIO是开源的对象存储服务,兼容S3协议,部署一个单机实例只需要下载一个二进制文件并运行。很多同学听到"对象存储"就觉得重,其实以MinIO的体量,配合spring-boot-starter-minio这类集成依赖,代码量比本地存储多不了多少,但换来的是图片管理的规范化:桶、对象、访问策略都是标准对象存储语义,以后迁移到云厂商的OSS、S3不需要改业务代码。标题热搜里也提到"minio加入到springboot",这确实是最近前后端分离项目里比较常见的需求。如果这个项目后续要扩展开源部署到多台服务器,我会毫不犹豫换成MinIO方案。
4.3 商品分页与多条件查询的实现
商品列表页是整个系统访问量最大的接口,核心要求是分页稳定、筛选条件可组合。我用MyBatis-Plus的分页插件实现,条件组合通过LambdaQueryWrapper完成。
public PageResult<ProductVO> pageProducts(ProductQueryVO query) { Page<Product> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 关键字搜索(标题模糊匹配) if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getTitle, query.getKeyword()); } // 分类筛选 if (query.getCategoryId() != null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 状态筛选,前台默认只显示审核通过且在售的商品 wrapper.eq(Product::getStatus, 1); // 按创建时间倒序 wrapper.orderByDesc(Product::getCreateTime); Page<Product> result = productMapper.selectPage(page, wrapper); // 类型转换和补充发布者信息... }这里有一个性能相关的细节:商品列表接口千万不要用selectList把全表数据查出来再在JVM里做分页,数据量一大直接内存溢出。MyBatis-Plus的selectPage会生成带LIMIT的SQL,分页在数据库层面完成,这才是正确的姿势。另外查询条件里status=1这个过滤条件必须加,穷举的时候你会发现,漏掉这个会导致已售出的商品还挂在列表里。
5. Vue前端从搭建到交互:路由守卫、Axios封装与页面复用
5.1 前端工程化搭建与依赖安装要点
前端工程我用Vite脚手架创建,命令npm create vite@latest校园二手-front -- --template vue即可。项目创建后第一件事是安装核心依赖:vue-router、pinia、axios、element-plus。Element Plus按需引入可以减少打包体积,但我建议学习阶段的同学直接全量引入,写起来省心,等做性能优化时再考虑按需加载。
这里聊聊vue安装及环境配置这块的经验。很多同学在配置Vue环境时反复出问题,十有八九是Node版本和工具链不匹配。我在文档里给的参考环境是:Node.js 18.16.0,npm 9.5.1,Vite 5.x。Node版本太低会导致Vite无法启动,太高(比如Node 21+)时偶尔会有依赖编译兼容报错。用nvm管理Node版本是IT圈比较省心的做法,可随时切换版本。
5.2 路由设计与登录守卫
前端路由是整个页面导航的地图。校园二手平台的路由分成两部分:前台页面(首页、列表页、详情页、个人中心、发布页)和后台管理页(工作台、用户管理、商品审核等)。我用Router把这两组路由直接写在同一个文件里,通过meta.role字段标注访问需要的角色。
登录守卫是前后端分离项目里前端安全的第一道防线。Vue Router的beforeEach钩子可以拦截所有路由跳转,核心逻辑是:如果目标页面需要登录但本地没有Token,跳转到登录页;如果目标页面需要管理员权限但本地用户信息里不是管理员,跳转到首页并给出提示。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.meta.role && to.meta.role !== userInfo.role) { next('/') } else { next() } })这里有个细节要提醒:localStorage.getItem('token')返回的是null而不能转成布尔值,很多同学在这里写if (localStorage.getItem('token') === null)就漏掉了key存在但值为空的情况。用!token这种方式判断最稳。
5.3 Axios封装与商品列表页交互
Axios拦截器是前端处理接口一致性的关键手法。我在项目里封装了统一的request.js:请求时自动附带Authorization请求头,响应时统一处理code字段,code为200就返回data数据,为401就清空本地登录信息并跳转登录页,其他错误码通过Element Plus的消息组件弹出后端返回的错误信息。这样一个封装写好,所有页面的网络请求代码都能精简成非常干净的三行式调用。
商品列表页是交互最复杂的页面,我拆成了三个子组件:SearchBar(关键字和分类筛选)、ProductList(商品卡片网格)、Pagination(分页条)。父组件维护状态,子组件通过props接收数据、通过emit抛事件。搜索时点击搜索按钮触发父组件的loadData方法,重新拉取第一页数据;分页条翻页时触发pageChange事件,用新的pageNum重新查询。完整交互链路跑通之后,你会发现这类中后台页面做多了,组件拆分的套路基本是一致的。
商品发布页的表单校验也要提一下:Element Plus的Form组件自带校验规则,我在发布表单上配置了必填校验和价格字段自定义校验函数(必须大于0的数字)。图片上传组件使用el-upload,配置action属性指向后端上传接口,headers里带上Authorization才能通过后端的登录校验——这个header配置很容易漏,漏掉的结果就是前端报401、图片死活传不上去。
6. 前后端联调与部署:跨域、打包、Tomcat/Nginx一次说清
6.1 开发环境跨域问题的三种解法
前后端分离的项目,联调时第一个撞上的就是跨域问题。浏览器的同源策略规定,当前端页面跑在http://localhost:5173,后端接口跑在http://localhost:8080时,两个端口不一致就被判定为跨域,浏览器会拦截后端返回的响应。
我在实践中有三种解法,按推荐度排序:
第一种是后端开启CORS跨域配置。在Spring Boot里加一个配置类,实现WebMvcConfigurer接口并重写addCorsMappings方法,允许指定路径、指定来源访问。这个方案对"前端开发环境固定"的情况最简单,几行代码解决。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }第二种是前端Vite开发代理。在vite.config.js里配置server.proxy,把/api路径的请求代理转发到8080端口。这个方案的优点是浏览器里看不到跨域,代码也更干净。开发阶段推荐这种方式。
第三种是Nginx反向代理,这同时也是生产环境的标准方案,放在6.3节详细说。
6.2 前端打包与后端jar包部署的两种组合方式
后端部署相对固定:在项目根目录执行mvn clean package,target下生成一个可执行的jar包,上传到服务器后执行java -jar target/校园二手-0.0.1-SNAPSHOT.jar,服务默认跑在8080端口。需要注意的是一定要确认服务器防火墙放过8080端口,或者用nginx把80端口转发到8080。
前端部署有两种组合方式,我分别验证过:
组合一:Nginx部署前端 + 反向代理后端。前端执行npm run build后生成dist目录,把这个目录上传到服务器,配置Nginx的root指向dist,location /api/段配置proxy_pass转发到后端的8080端口。这是最标准的生产部署方式,前端静态资源由Nginx高性能托管,后端动态接口通过代理转发。Nginx核心配置:
server { listen 80; server_name campus-market.example.com; root /usr/share/nginx/campus-market/dist; index index.html; # 前端路由history模式支持 location / { 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; } }组合二:把前端dist目录放到后端jar包里一起部署。Spring Boot支持把静态资源放在src/main/resources/static下,前端构建后的dist内容复制到static目录,重新打包后访问8080端口就能同时拿到前后端。适合小规模或演示场景,缺点是不太利于前端独立更新。
6.3 Tomcat部署与生产环境配置的注意事项
标题热搜里有tomcat部署前后端分离项目,这个需求在传统Java项目里很常见。虽然Spring Boot推荐直接用jar方式,但有些学校机房或者公司基础设施要求部署到外部Tomcat环境。操作方法也不复杂:把spring-boot-starter-tomcat的scope改为provided,启动类继承SpringBootServletInitializer并重写configure方法,最后打成war包放到Tomcat的webapps目录下。需要注意外部Tomcat和Spring Boot内置Tomcat的版本兼容性,建议用Tomcat 9或10。有一点提醒:外部Tomcat部署时前端dist依然放webapps的ROOT目录下,接口请求要注意上下文路径。
生产环境配置还有一个数据库时区问题需要提前处理。如果服务器默认时区是UTC,数据库连接串里的serverTimezone不设置的话,插入的时间会差8小时。我习惯把数据库连接URL统一加上serverTimezone=Asia/Shanghai,同时在Spring Boot配置里指定jackson的时间格式,前后端时间就能保持一致,不会出现"列表页显示的商品发布时间比实际慢了8小时"这种诡异问题。
7. 万字详设文档的价值与二次开发扩展方向
7.1 详设文档包含哪些内容
这套项目附带了一份万字详设文档,很多人觉得写文档是凑字数、走形式,但我自己的体会是:一份完整的详设文档对二次开发和答辩演示的价值,不亚于源码本身。文档里我按照软件开发的标准流程组织,包含以下章节:项目背景与可行性分析、需求分析(含用例图与用例说明)、系统功能结构设计、数据库详细设计(含ER图、每张表的字段说明)、接口设计文档(每个接口的URL、参数、返回值示例)、核心代码设计说明、系统测试报告、部署说明。
以数据库设计部分为例,文档不只是罗列字段,还解释了每个字段的选取理由和潜在的业务扩展点。比如商品表里单独设计的degree字段,如果以后要做筛选功能,直接在列表查询条件里加上eq就可以,不需要改表结构。这种设计和文档的结合,让使用者拿到项目后不只是"跑起来",而是真正理解为什么这样设计。
接口文档部分我做成了表格形式:接口地址、请求方式、请求参数、返回参数、备注。以商品发布接口为例,文档会标明POST /api/product/publish接口需要的参数(title、description、price、categoryId、images、degree),以及每种参数的类型和限制。前端照着这个文档联调,半小时就能接完一个页面。
7.2 基于源码的二次开发建议
拿到源码之后,我建议从这三个方向去做二次开发,既容易出成果又能真正练到技术。
第一,接入校内统一身份认证。目前系统用的是用户名密码注册,可以对接学校的统一身份认证平台(OAuth2或CAS协议),实现用学号直接登录。这个方向涉及OAuth2客户端配置、回调接口开发、用户信息映射,技术含量适中,做好之后切中校园场景的核心需求。
第二,引入在线支付。虽然面交是主流,但支持在线支付可以覆盖部分不想带现金和转账的场景。比较推荐的接入方案是微信支付Native支付(生成二维码)或沙箱环境。重点要理解下单流程、回调验签、订单状态同步这三个环节,支付回调的幂等性处理是关键中的关键,回调丢失后的对账方案也要提前设计。
第三,增加消息通知模块。当前系统里买卖双方的联系主要靠订单信息,比较被动。可以加一个简单的站内信或者系统公告推送,用WebSocket做实时通知,当有人收藏我的商品、我的商品被下单时,推送给卖方。这个方向能把WebSocket、异步消息、前端实时通信练一遍,在面试里也容易讲出亮点。
7.3 几个容易踩的坑与我的实测经验
最后分享几个我在开发和跑通这套系统过程中遇到的典型问题,每个都是实测踩出来的。
第一个坑是图片回显404。本地存储方案下,图片保存到了服务器磁盘,但Spring Boot默认不对外暴露file路径。新手最容易出现的错误是把图片URL写成localhost:8080/upload/xxx.jpg,然后发现404。解决方式是在yaml里配置静态资源映射,把upload目录映射到static-locations。
第二个坑是BeanUtils.copyProperties导致的时间字段格式化丢失。在把PO转换成VO时,直接用Spring的BeanUtils复制属性,如果PO里的时间是LocalDateTime、VO里的时间是String,复制时直接抛异常。处理办法是用Hutool的BeanUtil,或者在VO里也保持LocalDateTime类型,由全局Jackson配置统一格式化输出。
第三个坑是前端打包后路由404。Vue Router默认是history模式,打包部署到Nginx后直接访问某个子路由刷新,会返回404。原因很简单:Nginx找不到对应的静态文件。解决办法就是我在6.2节配置里的try_files $uri $uri/ /index.html,把请求都引导到index.html由前端路由接管。
第四个坑是数据库连接池失效。长时间运行后偶尔出现"Connection is not available with timeout"报错,常见原因是MySQL的wait_timeout参数默认8小时,连接池里的连接超过这个时间没被使用就会被服务端断开。解决办法是在连接串加上autoReconnect=true,或者使用HikariCP的keepalive-time配置。
说回到这个项目本身,校园二手交易平台看起来是一个常规的管理系统,但它覆盖了用户认证、商品管理、订单流转、文件存储、前后端交互、部署运维这几个Java全栈开发者必备的技术环节,麻雀虽小五脏俱全。我在实际整理这套代码和文档时的体会是:真正把一个项目做到能交付、能部署、能讲解,和只是能跑起来完全是两个层次,前者需要你把每个模块的设计理由都想透彻。如果大家在复现或二次开发的过程中遇到了文档里没有覆盖到的问题,欢迎在评论区留言交流。