ARTICLE DETAIL

资讯详情

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

校园二手交易系统实战:Spring Boot + 微信小程序核心设计解析

校园二手交易系统实战:Spring Boot + 微信小程序核心设计解析 很多刚接触Spring Boot和微信小程序开发的同学拿到“校园二手商品交易”这种选题时第一反应往往是不就是个增删改查嘛做起来应该很快。但实际上手之后才发现校园场景下的交易系统有它非常特殊的一面——不是所有用户都能登录、不是所有商品都能上架、不是所有订单都能用一套固定状态流转。这些业务上的“潜规则”远比CRUD本身更值得花精力去设计。这篇文章我会从我做这个项目的完整路径出发把校园二手交易系统从需求拆解到后端核心实现、再到小程序端的关键交互逐个环节讲清楚。我会重点讲微信登录中code换token的完整链路、订单状态机的设计思路、商品发布的审核逻辑以及一些你在文档里不太容易看到的坑——比如Spring Boot版本选型对后续开发的影响、小程序scroll-view内嵌uni-datetime-picker的渲染异常还有线上环境HeapDump泄露风险这一类安全问题。适合正在做毕设、准备找实习项目的同学也适合想从前端角度理解Spring Boot后端设计的开发者。1. 校园二手交易它和普通电商系统最不一样的地方在哪里很多人在设计这类系统时习惯性地套用淘宝、京东的模型——用户注册、商品上架、下单支付、物流发货。但放到校园场景里这套逻辑至少有三个方面是走不通的。1.1 用户身份是强约束下的资源校园二手交易的核心特征是信任半径有限。买家和卖家大概率是同一所学校甚至同一栋宿舍楼的人这和陌生人电商有本质区别。所以用户体系不能像普通商城那样随意注册个手机号就能用而是必须绑定校园身份。我在设计时采用的是微信小程序登录获取openid再辅以学号认证的方式。这里有个容易被忽视的点微信小程序登录拿到的openid是用户在某个小程序下的唯一标识但它本身不包含任何真实身份信息。所以系统里必须单独设计一张用户信息表把openid和学生认证信息分开存储。认证状态用枚举值管理——未认证、待审核、已认证、认证失败而不是简单的布尔值这样后续做“认证通过后才能发布商品”这类规则时会更灵活。1.2 商品流转逻辑比电商更轻盈但状态更多校园二手交易的体量不大但状态变化非常频繁。一件商品可能经历“发布→审核→上架→被预约→下架/卖出”等过程。和电商系统不同这里没有购物车、没有库存并发抢购核心流转其实围绕“预约”和“成交”两个动作展开。所以在数据库设计时我没有照搬电商的订单模型而是为二手交易单独设计了一套基于订单状态机的表结构。这块细节我会在后面的实操章节展开这里先给一个结论状态流转不能靠代码里散落的if-else去改status字段而应该把状态机单独抽出来管理。1.3 “生活信息服务”是加分项也是区分度原标题里还有一个容易忽略的关键词——“生活信息服务”。这意味着系统不只是卖二手货还可以承载失物招领、校园拼车、求购信息、兼职发布等轻量级信息流。这部分我在开发时作为独立模块实现复用了商品模块的展示框架但业务逻辑完全分开。这样做的好处是既能在演示时展示更多功能页面又不会让二手交易的核心链路因为信息流模块而变得复杂。2. 技术选型与整体架构为什么是Spring Boot 微信小程序而不是别的组合这个项目的技术栈选择牵扯到很多热搜词里提到的疑点。比如有人问“springboot版本太高会不会有问题”还有人纠结“springboot自动装配到底是怎么工作的”。我在这里把选型逻辑和环境准备一次说清楚。2.1 Spring Boot版本选择的现实考量我在项目里用的是Spring Boot 2.7.x而不是最新的3.x。原因很简单生态兼容性。校园项目中常见的依赖比如MyBatis-Plus、微信支付SDK、某些老牌工具库它们对Spring Boot 2.x的适配非常成熟但到了3.x尤其是Jakarta命名空间切换后可能就需要额外处理。如果你是做毕业设计或者想快速跑通一个能演示的项目没有必要为了“追新”去踩兼容性的坑。如果你确实想用Spring Boot 3.x至少要确认三件事MyBatis-Plus版本是否支持Spring Boot 3需要3.5.3是否有老代码依赖了javax.*包3.x已改为jakarta.*内嵌Tomcat的版本是否和本地JDK版本匹配2.2 项目整体分层设计我采用的前后端分离结构里Spring Boot只负责提供RESTful API微信小程序单独作为一个前端工程。这里的“前后端分离”指的是代码仓库和部署上分离但在开发调试阶段小程序端要访问本地后端接口时需要在微信开发者工具里关闭域名校验或者在后端配置好CORS跨域。后端项目的包结构我建议这样分com.campus.secondhand ├── controller # 接口层只做参数接收和结果封装 ├── service # 业务逻辑层事务控制和状态流转都在这层 ├── mapper # MyBatis-Plus数据访问层 ├── entity # 数据库实体类 ├── dto # 接收前端参数的模型避免直接暴露实体 ├── vo # 返回给前端的视图模型比如脱敏后的用户信息 ├── config # 配置类比如微信配置、拦截器配置 ├── utils # 工具类比如JWT工具、微信请求工具 └── common # 公共类比如统一返回结果封装这样的分层明确之后很多问题会自然消失。比如有人问“修改刚进入的加载页面”怎么做——在小程序端是改app.json里的entryPagePath在后端则是网关或拦截器层面的问题两者不冲突。2.3 为什么需要一个合理的统一返回结构我做后端接口时第一步不是写业务而是定义统一返回结果类。每个接口都返回固定的JSON结构{ code: 200, message: success, data: {} }这样小程序端就可以用同一套逻辑处理成功、失败、未登录等不同情况。配合全局异常处理器后端代码里就不需要到处写try-catch了。这个小细节对前端联调非常友好强烈建议在一开始就做好。3. 数据库设计核心表结构与状态字段的取舍思路这部分的中心是哪些表必须单独建哪些字段必须用状态值而非文本存储。我按照业务模块分开讲。3.1 用户表openid、学号与认证信息的组合用户表是整张业务网的起点。我的设计是字段名类型说明idbigint主键自增openidvarchar(64)微信小程序唯一标识唯一索引nicknamevarchar(64)用户昵称avatar_urlvarchar(255)头像地址student_novarchar(32)学号可为空real_namevarchar(32)真实姓名认证后写入auth_statustinyint0未认证1待审核2已认证3认证失败phonevarchar(16)联系电话create_timedatetime注册时间一个容易被忽略的问题用户授权头像和昵称的接口在小程序中有严格限制以前那种wx.getUserInfo弹窗授权的方案已经不行了现在推荐使用button的open-typechooseAvatar和nickname输入框来实现。这块如果不注意真机预览时会发现头像和昵称无法获取。3.2 商品表交易状态与审核状态要分开管理很多初学同学喜欢把“审核状态”和“交易状态”塞进同一个字段里用0、1、2、3来表示。但这样后期会越改越乱。审核状态描述的是“平台对商品的信任度”交易状态描述的是“商品当前的流通阶段”两个维度是不同的。我的商品表核心字段设计如下字段名类型说明idbigint主键user_idbigint发布者ID关联用户表titlevarchar(100)商品标题descriptiontext商品详细描述pricedecimal(10,2)价格original_pricedecimal(10,2)原价用于展示折扣力度categoryvarchar(32)分类编码imagesvarchar(1000)商品图片多张用逗号分隔statustinyint0草稿1待审核2在售3已被预约4已售出5下架audit_statustinyint0未提交审核1审核中2通过3拒绝audit_reasonvarchar(255)审核拒绝原因view_countint浏览量这里要特别说明images字段。二手商品通常包含多张图新建一个附件表是最规范的方式但如果项目周期短用逗号分隔图片地址可以极大减少联表查询。我在项目里是用了附件表的方式因为后续“生活信息服务”模块里也需要上传图片一张通用的附件表可以复用。商品表中最关键的其实是status字段。我在项目里维护了一个OrderStatusHandler状态机类专门负责校验状态是否允许流转。后面会详细讲。3.3 订单表围绕状态机设计而不是围绕支付设计校园二手交易不一定涉及到线上支付。很多交易的最终成交可能是在线下完成的比如在食堂门口当面付款。因此订单表设计时我的思路是以“预约-确认-完成”为核心线上支付作为可选项。订单表关键字段字段名类型说明idbigint主键order_novarchar(32)订单编号业务上显示用product_idbigint商品IDseller_idbigint卖家IDbuyer_idbigint买家IDstatustinyint0待付款1已付款2已完成3已取消pay_typetinyint0线上支付1线下当面交易contact_addressvarchar(255)线下交易地点可为空create_timedatetime创建时间finish_timedatetime完成时间这里交易和电商不一样的地方在于一个商品同一时间只能有一个有效预约。所以在用户点击“我想要”时后端要同时完成两件事检查商品状态是否为“在售”并锁定改状态为“已被预约”同时创建订单。这个操作必须在事务中完成否则会出现两个买家同时抢到预约名额的情况。4. 后端核心功能实现微信登录、JWT鉴权与状态机设计这一部分进入真正的编码环节。我会把微信小程序“用code换token”这条完整链路拆开讲同时把Spring Boot自动装配原理中与本次开发相关的部分做一个通俗解释方便面试时也能讲明白。4.1 微信登录完整链路为什么不能把code直接拿来做身份标识很多教程里写得比较粗糙大概流程是前端wx.login()拿到code发给后端后端拿code去微信接口换openid然后返回一个自定义登录态。但这里面有几个关键细节值得展开。步骤一前端获取code在小程序端添加一个登录按钮点击后调用wx.login()。这个code的有效期只有5分钟且一次性的使用后立即失效。所以后端必须尽快用掉它。wx.login({ success: async (res) { if (res.code) { // 将code发送到后端 const loginRes await request.post(/api/auth/login, { code: res.code }); // 存储后端返回的token wx.setStorageSync(token, loginRes.data.token); } else { console.log(登录失败, res.errMsg); } } });注意这里前端不应该拿code做任何本地解析它只是一个临时的授权凭证真正的身份信息交换发生在后端和微信服务器之间。步骤二后端用code换取openid和session_key后端接口先接收前端传来的code然后向微信服务器发起请求。这个接口就是你打开微信公众平台能看到的那套jscode2session接口。// 使用RestTemplateSpring Boot 2.7时代或HttpClient public WxSessionResult code2Session(String code, String appId, String appSecret) { String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; String resultStr restTemplate.getForObject(url, String.class); // 解析 resultStr包含 openid、session_key、unionid 等字段 return JSON.parseObject(resultStr, WxSessionResult.class); }这里有个重要提醒不要把appSecret直接暴露在小程序端代码里。appSecret是后端持有的密钥一旦暴露别人就可以冒充你的小程序身份去请求微信接口。正确方式是把appId和appSecret配置在后端Nacos或application.yml中。请求微信接口成功后会返回openid和session_key。其中session_key是用于解密手机号、运动数据等敏感信息的本项目暂时用不到但还是建议在后端保存起来以防后续扩展。步骤三自定义登录态token拿到openid之后先去数据库查这个用户是否存在。如果不存在就创建一个新用户此时nickname和avatar可以先为空等用户完善个人资料时再补全。如果存在就读取该用户当前的认证状态。然后后端生成一个自定义登录态token——我用的是JWT里面只存userId和openid加上过期时间用HMAC-SHA256签名。这个token返回给前端后前端在后续每次请求时放入请求头Authorization: Bearer token后端通过拦截器解析出用户身份。// JWT生成逻辑仅示意核心代码 String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(openid, openid) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7L * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();步骤四为什么这个方案安全整个链路有一个容易被忽略的核心逻辑code只是临时的、一次性票据它只用来向微信服务器换取真正的用户标识openid。后端拿到openid后并不是每次请求都去校验openid而是签发自己的token后续请求全部基于token体系。这样做的好处是openid不直接暴露在每次请求中降低被恶意抓包后冒充的风险token有有效期可以控制会话生命周期后端更换签名密钥或踢人下线时只需要调整token生成或校验逻辑4.2 Spring Boot自动装配原理它到底“自动”了什么很多初学者在配置Spring Boot时总觉得它很神奇——明明没写几行配置却能自动连接数据库、自动处理JSON、自动配置拦截器。这背后就是热搜词里反复出现的“springboot自动装配原理”。我打个比方Spring Boot就像一个酒店大堂你开发者只要告诉前台“我要一间带办公桌的房间”酒店就会自动帮你配好桌子、椅子、台灯、网线口你不需要自己去仓库一件件搬。这就是自动装配的意思——通过依赖中的META-INF/spring.factories文件里声明的配置类Spring Boot在启动时根据你引入的依赖自动注册相关的Bean。以spring-boot-starter-web为例它引入了spring-web、spring-webmvc以及内嵌Tomcat。Spring Boot的DispatcherServletAutoConfiguration会自动帮我们创建DispatcherServlet、RequestMappingHandlerAdapter等组件所以我们直接用RestController和GetMapping就能跑起接口。在实际项目里有两个地方体现了对自动装配的理解如果自定义了一个拦截器或过滤器要注意它们的注册顺序和生效范围。Spring Boot把Bean管理的很好但如果你同时注册了多个Filter执行顺序很容易搞混。如果想覆盖Spring Boot默认的JSON序列化规则比如把Long类型序列化为字符串防止前端精度丢失你需要自己定义一个Jackson2ObjectMapperBuilderCustomizer并注册为Bean它会自动被Spring Boot拾取。所以学习和理解自动装配原理对于一个实习项目来说最大的意义在于你知道去哪里覆盖默认行为而不是被默认行为坑得不可自拔。4.3 订单与商品状态机的谨慎实现刚才提到状态不能靠散落的if判断去维护下面展示一个核心状态机设计思路。我定义了一个ProductStatusHandler接口每个状态它关注两个方法supportTransition(from, to)和handleTransition(...)。这样当商品从“在售”要流转到“已被预约”时前端会请求订单创建接口后端在事务内先检查当前商品状态是否为“在售”再把状态改为“已被预约”最后创建订单。Service public class ProductStatusService { private final MapString, ListString TRANSITIONS new HashMap(); public ProductStatusService() { // 定义允许的状态流转例如在售-已被预约在售-下架等 TRANSITIONS.put(ON_SALE, Arrays.asList(RESERVED, OFF_SHELF, SOLD)); TRANSITIONS.put(RESERVED, Arrays.asList(SOLD, OFF_SHELF, ON_SALE)); TRANSITIONS.put(OFF_SHELF, Arrays.asList(ON_SALE)); } public void changeStatus(Product product, String targetStatus) { String currentStatus product.getStatus(); ListString allowed TRANSITIONS.get(currentStatus); if (allowed null || !allowed.contains(targetStatus)) { throw new BusinessException(非法的状态流转: currentStatus - targetStatus); } // 执行更新并记录操作日志 } }实际开发中还可以在这个基础上加状态变更记录表把每一次状态变化的时间、操作人都存下来。如果后续要排查“为什么商品被下架了”有日志可以直接定位。4.4 基于HandlerInterceptor实现登录鉴权小程序端大多数请求都需要登录态。我在后端实现了一个AuthInterceptor作用是在进入Controller之前检查请求头里的Authorization。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { // 解析JWT并放入ThreadLocal或Request Attribute String realToken token.substring(7); Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(realToken).getBody(); request.setAttribute(userId, Long.valueOf(claims.getSubject())); return true; } // 未登录返回401状态码和统一JSON response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } }然后在WebMvcConfigurer中注册拦截器并指定拦截和不拦截的路径Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/product/list, /api/product/detail/**); } }这样写的好处是不需要在每个Controller方法里重复判断是否登录。接口方法中通过RequestAttribute(userId) Long userId就能拿到当前登录用户ID。5. 小程序端开发中的关键设计界面适配、本地存储与组件踩坑小程序端并不是简单套一个前端框架它有一些平台特有的细节必须处理。这一节我会结合开发过程中印象深刻的几个点来展开。5.1 自定义顶部导航栏高度不同机型不再歪楼很多同学会遇到“微信小程序顶部导航栏高度”的问题。默认情况下小程序页面顶部会有一个胶囊按钮胶囊里包含“...”和“○”不同机型的胶囊高度、顶部状态栏高度都不一样。如果用默认导航栏其实不用关心这个问题但如果你想让顶部标题和背景颜色更自由就必须使用navigationStyle: custom然后自己计算导航栏高度。计算方式如下const systemInfo wx.getSystemInfoSync(); const menuButtonInfo wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 menuButtonInfo.height;这个公式本质上是胶囊按钮上沿到状态栏底部的距离的两倍加上胶囊自身高度得到一个大致等于自定义导航栏的总高度。5.2uni-datetime-picker在scroll-view里无法弹出的问题这个坑我印象非常深刻。在scroll-view内使用uni-datetime-picker时会出现点击后选择器不弹出的现象。我查了很久最后发现是iOS端的渲染机制问题。简单来说scroll-view作为一个滚动容器会创建独立的渲染层而uni-datetime-picker弹出层默认挂载在页面底部。在iOS中如果滚动容器内部有事件阻止了冒泡或者overflow的计算出现偏差弹出层就被挡在了滚动区域之外。解决思路有两个方向不在scroll-view内直接使用popup类型组件改用页面级picker微信原生组件不过样式上会有所妥协。如果确实需要uni-datetime-picker尝试将is-popover属性设为false强制其在同一渲染层级展示。实际上最稳妥的方案是把datetime-picker放在滚动区域外部通过按钮或输入框触发时定位到页面顶层显示。虽然麻烦一点但能避免iOS端的兼容性问题。5.3 下拉刷新、加载更多的实现思路校园二手交易的信息流页面需要支持下拉刷新和触底加载更多。小程序里实现方式非常直接在页面json里开启enablePullDownRefresh: true监听onPullDownRefresh回调重新请求第一页数据结束后调用wx.stopPullDownRefresh()触底加载是通过onReachBottom监听每次追加page1的数据我习惯用page和size两个参数管理分页。后端返回一个包含list和total的对象{ records: [], total: 30, page: 1, size: 10 }如果records数量小于size说明没有更多数据了前端可以停止继续加载。5.4 条件渲染的关键项用户认证状态而不是用户是否登录首页和个人中心的展示逻辑要区分三种状态未登录、已登录但未认证、已认证。为什么强调这个因为校园二手交易中“认证后才有权限发布商品”是平台规则的核心。如果你只做if (loggedIn)判断那么用户登录后就能看到发布按钮点击发布却被告知“未认证”这种交互体验非常差。正确做法是用户模型返回时带上authStatus发布按钮的显隐直接由authStatus 2控制如果未认证点击则跳转到认证页面页面里清晰说明认证流程和预计审核时间6. 商品发布与订单流转的完整案例从用户提交到订单完成为了把上面的设计串起来我以“买家预约购买二手教材”为例子走一遍后端接口的完整调用链。6.1 商品发布流程卖家从小程序端填写商品信息包括标题、描述、价格、分类、图片然后点击提交发布。前端请求POST /api/product/publish { title: 高等数学第七版上册, description: 同济大学出版内页有少量笔记不影响阅读, price: 15.00, originalPrice: 42.00, category: BOOK, images: [https://cdn.xxx.com/1.jpg, https://cdn.xxx.com/2.jpg] }后端处理逻辑从请求头解析出userId查询该用户的auth_status如果不是2已认证直接返回“请先完成校园认证”校验商品标题长度、价格范围、图片数量控制在1~9张将图片地址存到attachment表并把附件ID回填到product.images初始化商品audit_status0未提交审核status0草稿注意这里我并没有在发布时就直接把商品设置为在售状态。因为需要后台管理员审核。所以“发布”动作其实分两步第一步是保存草稿第二步是提交审核。在小程序端用户点击“发布”时其实是先调用/api/product/publish保存草稿再调用/api/product/submitAudit将auditStatus改为1。6.2 买家预约流程买家在商品详情页看到“在售”状态的商品点击“我想要”按钮。前端请求POST /api/order/create { productId: 1 }后端处理逻辑查询商品确认状态为“在售”status2校验当前用户不是商品发布者本人不能购买自己发布的商品开启事务将商品状态改为“已被预约”status3创建一条新订单seller_id取商品的user_idbuyer_id取当前用户ID返回订单ID给前端这里我要单独说一下事务陷阱。很多新手会在Controller里先查商品、再更新商品、再创建订单但没有加Transactional。如果创建订单失败商品状态可能已经被改成了“已被预约”用户会误以为有人预约了但实际上订单不存在这属于严重的脏数据问题。我的建议是所有涉及多个表写入的操作统一在Service层加上Transactional(rollbackFor Exception.class)。6.3 卖家确认交易与订单完成当买家预约后双方可以通过平台交换联系方式如微信号线下验货、付款。此时订单状态有以下几种走向买家取消预约将商品状态改回“在售”订单状态改为“已取消”卖家在订单详情中点击“确认成交”说明交易已完成订单状态改为“已付款”或“已完成”商品状态改为“已售出”status4如果双方聊崩了卖家可以点击“关闭交易”把商品放回“在售”状态供其他人预约这里就有一个防御性校验订单创建后除了最初创建订单的接口外任何状态变更都要求当前登录用户必须是订单的buyer_id或seller_id之一否则报“无权操作”。6.4 后台审核校园二手交易平台通常会有一个管理员端如果想做web管理后台可以在Spring Boot中单独建一个admin模块部署时用独立的访问路径。管理员可以查看待审核商品列表点击“通过”或“拒绝”拒绝时需要填写理由。审核通过后商品状态自动从“草稿”变为“在售”。这个审核动作同样要走状态机audit_status从1审核中到2通过或3拒绝。审核通过后还要触发商品status从0草稿变为2在售。7. 安全加固与易踩的坑从HeapDump到版本兼容这一节是比较容易被忽视但实际影响很大的部分。搜热词的时候看到“springboot heapdump 敏感信息泄露漏洞”这个我在项目中也踩到了这里展开讲讲。7.1 为什么HeapDump会成为漏洞Spring Boot Actuator提供了非常有用的运维端点比如/actuator/heapdump可以下载当前JVM的堆内存镜像。如果把这个端点暴露到公网且没有鉴权任何人都可以下载下来然后用MAT或JDK自带的jhat分析内存中的对象。内存里可能包含用户的token、密钥、数据库连接信息等敏感数据。在这个项目中我不建议直接在生产环境暴露Actuator端点。如果确实需要监控可以用以下两种方式只允许内网IP访问Actuator端点设置management.endpoints.web.exposure.includehealth,info只暴露无风险的端点heapdump和shutdown一律关闭在application.yml中management: endpoints: web: exposure: include: health,info7.2 使用统一的权限校验与参数校验除登录鉴权之外参数层面也要做好校验。我在所有Controller方法的DTO入参上都加了Validated注解同时在实体字段上添加NotNull、NotBlank、Size等校验规则。这样非法请求会在进入业务逻辑前就被拦截而不是等到数据库操作时报错才暴露。7.3 关于“charles抓包电脑端微信小程序”抓包是调试小程序网络请求的常用手段。用Charles抓小程序包时经常遇到HTTPS证书信任问题。这里建议在开发环境中在微信开发者工具中勾选“不校验合法域名”在“工具——安全设置”中开启“允许HTTPS抓包”电脑端系统需要安装并信任Charles的根证书但抓包也提醒我们一件事既然是HTTPS加密传输就不要在URL里明文传token应该放在请求头中并确保后端拦截器第一个就能校验它。7.4 Spring Boot版本太高带来的另一个隐患关于“springboot版本太高”的热搜词很多人问“是不是越高越好”。我前面说过校园项目优先选2.7.x。还有一个隐患是Spring Boot高版本中内嵌Tomcat的默认行为也会变。比如在高版本中Tomcat对RESTful API的路径匹配规则可能更严格一些模糊路径会直接返回404。如果前端习惯在URL末尾加斜杠就可能出现“这个接口本地能行部署到线上却404”的现象。这种问题排查起来很费时间所以选型时尽量选择和教程、依赖生态匹配的版本避免无谓的烦恼。7.5 本地图片存储还是对象存储上传图片时我一开始把图片存在本地磁盘通过后端的静态资源映射来访问。这样做在本地测试没问题但部署到服务器后如果项目重启或文件被清理图片可能就丢了。而且如果未来要做负载均衡分布在多台机器上的图片文件无法共享。所以我在项目里直接选择了对象存储方案。如果只是为了毕业设计和演示可以使用国内云服务商的免费额度或者用MinIO搭建本地对象存储服务。小程序的image组件可以直接展示对象存储的URL后端只需要保存这个URL字符串即可。8. 部署上线与后续优化方向从开发环境到可演示的完整项目最后聊一下怎么把项目从本地环境部署到可演示的状态。8.1 后端部署我推荐用云服务器 Docker Compose来部署。后端镜像拉起来后需要注意以下几点数据库用云数据库或服务器自建MySQL记得开放3306端口给服务器内网但不要对公网开放Redis用于缓存热门商品列表、用户token黑名单如果需要也要设置密码配置Spring Boot的application-prod.yml区分生产环境配置和本地配置使用Nginx反向代理后端接口配置HTTPS证书这样小程序端才能合法请求8.2 小程序发布小程序提交审核时要注意类目选择。校园二手交易涉及交易功能但目前不涉及特殊资质审核。发布时要注意小程序后台需要配置合法域名如果使用对象存储的域名也要一并配置到downloadFile合法域名中测试时使用体验版二维码审核时最好录制一段演示视频方便审核人员理解8.3 后续优化方向如果想把项目做得更完整可以在现有基础上增加以下功能基于用户收藏行为做个性化推荐商品搜索中的关键词联想可以使用HanLP分词后在标题、描述中匹配消息通知通过小程序订阅消息通知用户“商品被预约”“审核结果”等状态变化通过定时任务定期清理超时未交易的预约订单释放商品库存预约状态超过48小时自动取消其中订阅消息是一个很好的亮点。微信小程序订阅消息需要用户在特定动作下授权比如提交预约时弹出订阅授权授权后后端通过调用发送订阅消息接口通知用户。这块如果能跑通面试时可以讲的内容会丰富很多因为它同时涉及前端交互、后端调用微信接口、以及消息队列的削峰思路。以我个人的开发体会来说校园二手交易系统最核心的难点不在于代码量而在于对业务规则的抽象。把用户认证、商品审核、订单状态流转这些约束想清楚代码写起来会非常顺。相反如果一开始就急着堆CRUD后面会不断被各种状态组合折磨。这份经验希望能帮你少走一些弯路。
返回列表