简介:本资源是一套完整的校园二手交易平台源码,面向计算机专业学生、全栈开发初学者及小程序实践者,解决高校场景下闲置物品高效流转与轻量化交易管理的实际需求。项目采用Spring Boot构建后端服务,uniapp实现跨端微信小程序前端,涵盖用户浏览购买、商家商品发布与沟通、后台多角色权限管理三大核心模块,具备完整MVC分层结构与可部署性。压缩包共1444个文件,含145个Java后端逻辑文件、266个Vue/uniapp组件(.vue)、174个JS交互脚本、157个JSON配置与接口定义、134个PNG图标资源及58个WXML/WXSS页面样式文件,整体25.24MB,结构清晰、注释规范,便于理解前后端协同机制与小程序生命周期管理。目前已有110人学习下载,提供可直接运行的工程骨架、数据库SQL脚本、三端(用户/商家/管理员)功能闭环及.bat一键启停脚本,适合用于课程设计、毕业设计或技术栈整合实战。
1. 项目概述:一个校园里的“闲鱼”是怎么炼成的
最近有不少学弟学妹来问我,想做一个校园二手交易平台练手,有没有什么现成的、能跑起来的项目可以参考。正好,我之前带团队做过一个基于Spring Boot后端和uniapp前端的校园二手交易微信小程序,从零到一完整走了一遍,踩了不少坑,也积累了不少心得。这个项目麻雀虽小,五脏俱全,涵盖了用户系统、商品发布、搜索、聊天、订单管理、支付(模拟)等核心功能,非常适合作为全栈学习的入门或进阶项目。今天,我就把这个项目的核心设计思路、技术选型的考量、关键模块的实现细节,以及那些“教科书上不会写”的实操坑点,系统地梳理一遍。无论你是想学习Spring Boot如何构建RESTful API,还是想了解uniapp如何高效开发多端应用,亦或是想搞明白前后端如何协同工作,这篇文章都能给你提供一份可以直接“抄作业”的实战指南。
2. 整体架构与核心技术选型解析
2.1 为什么是Spring Boot + uniapp + 微信小程序?
当初立项时,技术栈的选定我们经过了多轮讨论。核心目标很明确:快速开发、易于部署、学习成本低、并且能真正在校园环境里用起来。
后端选择Spring Boot的理由:
- 约定大于配置:这是最吸引我们的点。校园项目开发周期短,团队成员水平参差不齐,Spring Boot通过自动配置和起步依赖,极大地简化了SSM(Spring+SpringMVC+MyBatis)那一套繁琐的XML配置。我们只需要在
pom.xml里引入spring-boot-starter-web,spring-boot-starter-data-redis,mybatis-spring-boot-starter等依赖,一个具备Web、数据库访问、缓存能力的基础后端框架就搭好了。 - 内嵌容器,独立运行:传统的Java Web项目需要打包成WAR包,部署到外部的Tomcat服务器。Spring Boot应用可以直接打包成可执行的JAR文件,内嵌了Tomcat(默认)、Jetty或Undertow,通过
java -jar命令就能运行。这对学生团队来说太友好了,本地测试、演示部署都非常方便。 - 生态繁荣,社区强大:Spring Boot背后是庞大的Spring生态,几乎你遇到的所有问题,都能在社区找到解决方案或轮子。比如集成Redis做缓存和会话管理、集成Swagger做API文档、集成Spring Security做权限控制(我们这个项目初期为了简化,自己实现了简单的Token鉴权),都有非常成熟的方案。
前端选择uniapp + 微信小程序的理由:
- 一套代码,多端发布:uniapp的核心优势。我们的主要目标是微信小程序,但保不齐未来学生会想要一个H5页面或者App。使用uniapp,我们只需编写一套Vue.js代码,通过条件编译,就可以发布到微信小程序、H5、App等多个平台。这大大降低了多端适配的成本。
- 开发体验接近Vue:团队成员有Vue基础,上手uniapp几乎没有障碍。其组件化开发、数据驱动视图的理念与Vue一脉相承,学习曲线平缓。
- 微信小程序生态成熟:在校园场景下,微信的渗透率接近100%。小程序无需下载安装、即用即走的特性,非常适合二手交易这种低频、轻量的应用。微信提供了完善的登录、支付、消息订阅等能力,虽然我们项目里支付是模拟的,但接口设计上预留了对接真实微信支付的可能性。
注意:技术选型没有银弹。这个组合适合快速原型和校园级应用。如果项目复杂度极高、并发量很大,可能需要考虑Spring Cloud微服务、更精细的前端框架选型(如React Native for App)等。但对于我们90%的场景,这个组合是性价比最高的。
2.2 系统核心模块设计
在动手写代码之前,我们花了几天时间画流程图、设计数据库表结构。清晰的模块划分是项目不跑偏的基石。整个系统主要分为以下几个核心模块:
- 用户模块:处理微信一键登录、用户信息管理(头像、昵称、手机号)、信用分体系(如交易成功加分,被投诉扣分)。
- 商品模块:核心中的核心。包括商品分类管理、商品发布(标题、描述、多图上传、价格、位置)、商品状态管理(上架、下架、已售出)、商品搜索与筛选。
- 交易模块:模拟交易流程。包括生成订单、订单状态流转(待付款、待发货、待收货、已完成、已取消)、模拟支付接口、确认收货逻辑。
- 即时通讯模块:买卖家沟通的桥梁。我们采用了WebSocket实现简单的点对点聊天,支持文本、图片。消息本地存储,并与用户和商品关联。
- 后台管理模块:一个简单的PC端管理后台(基于Vue+Element UI),用于管理员审核商品、处理投诉、管理用户等。
数据库设计要点:
- 用户表(
user):除了基础信息,关键字段有openid(微信唯一标识)、session_key、credit_score(信用分)。 - 商品表(
product):title,description,price,category_id,seller_id(关联用户),status(0-待审核,1-上架中,2-已售出,3-已下架),location(发布位置,如“梅园宿舍区”)。 - 商品图片表(
product_image):与商品表一对多关联,存储图片URL。这里没有直接把图片存数据库,而是存了上传到云存储(如腾讯云COS)后的链接。 - 订单表(
order):order_sn(唯一订单号,我们用了时间戳+随机数生成),product_id,buyer_id,seller_id,total_price,status(0-待付款,1-待发货,2-待收货,3-已完成,4-已取消)。 - 聊天消息表(
chat_message):sender_id,receiver_id,content,msg_type(text/image),product_id(关联到具体商品,方便上下文追溯)。
3. 后端Spring Boot核心实现与踩坑实录
3.1 项目结构规划与配置
一个清晰的项目结构能让团队协作更顺畅。我们采用的是典型的MVC分层结构:
src/main/java/com/campus/trade/ ├── CampusTradeApplication.java // 启动类 ├── config/ // 配置类,如WebMvcConfig, RedisConfig, MybatisConfig ├── controller/ // 控制器层,接收请求,返回JSON ├── service/ // 业务逻辑层,接口和实现分离 │ ├── impl/ ├── mapper/ // MyBatis的Mapper接口层,对应dao ├── entity/ // 实体类,与数据库表对应 ├── dto/ // 数据传输对象,用于前后端交互,如请求/响应封装 ├── utils/ // 工具类,如JWT工具、日期工具、云存储上传工具 └── common/ // 通用类,如统一返回结果封装、异常定义、常量关键配置解析:
application.yml:我们放弃了application.properties,因为YAML的层次结构更清晰。这里配置了数据源、Redis、MyBatis、文件上传大小限制、服务器端口等。spring: datasource: url: jdbc:mysql://localhost:3306/campus_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 10MB # 限制单文件大小 max-request-size: 50MB # 限制总请求大小 mybatis: mapper-locations: classpath:mapper/*.xml # MyBatis的SQL映射文件位置 configuration: map-underscore-to-camel-case: true # 自动将下划线字段映射为驼峰属性 server: port: 8080统一响应封装:为了前后端协作规范,我们定义了一个
Result类。@Data public class Result<T> { private Integer code; // 状态码,如200成功,400客户端错误,500服务器错误 private String msg; // 提示信息 private T data; // 响应数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("success"); result.setData(data); return result; } // 其他静态方法如 error, fail... }这样,所有Controller的方法都返回
Result<T>,前端处理起来非常一致。
3.2 微信登录与用户鉴权实战
这是小程序与后端交互的第一个关口,也是最容易出错的地方。
流程简述:
- 小程序端调用
wx.login()获取临时凭证code。 - 小程序端将
code发送给我们的Spring Boot后端。 - 后端用
appid,secret和code,调用微信接口服务https://api.weixin.qq.com/sns/jscode2session,换取openid和session_key。 - 后端根据
openid判断用户是否首次登录。若是,则在数据库创建用户记录;若不是,则更新最后登录时间。 - 后端生成一个自定义的Token(我们用了JWT),将
openid、userid等信息存入,并返回给小程序。 - 小程序后续请求,在HTTP Header的
Authorization字段携带此Token。 - 后端通过一个拦截器(
HandlerInterceptor)或过滤器(Filter)验证Token的有效性,并从中解析出用户信息,存入请求上下文(如ThreadLocal),供业务层使用。
踩坑与解决方案:
- 坑点1:
session_key泄露风险。session_key是微信端的会话密钥,绝对不能通过网络传输给前端!它只应存在于后端。我们用它来解密前端获取的加密数据(如手机号)。 - 坑点2:Token过期与刷新。JWT Token一旦签发,在有效期内无法废止。我们设置了较短的过期时间(如2小时),并设计了刷新Token的机制。当Token过期,前端用特定的刷新Token(一个存在Redis里、关联用户ID的随机字符串)来换取新的Access Token。
- 坑点3:并发登录。同一个用户多次快速登录,可能会并发创建多条用户记录。我们在数据库的
openid字段上加了唯一索引,并在业务代码里用synchronized关键字或数据库的INSERT ... ON DUPLICATE KEY UPDATE语句做了处理。
核心代码片段(JWT工具类):
@Component public class JwtUtil { @Value("${jwt.secret}") // 从配置读取密钥 private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(String userId, String openId) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expire * 1000); return Jwts.builder() .setSubject(userId) // 通常用用户ID .claim("openId", openId) // 自定义声明 .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } // ... 其他验证方法 }3.3 商品模块与图片上传的细节
商品发布是核心功能,其中多图上传和存储是难点。
后端图片上传处理:
- 接口设计:我们单独提供了一个
/api/upload/image的POST接口,接收multipart/form-data格式的文件。 - 云存储选择:为了减轻服务器带宽和存储压力,我们没有把图片存在服务器本地,而是上传到了腾讯云对象存储COS(因为小程序生态和腾讯云结合较好)。阿里云OSS、七牛云等都是可选方案。
- 上传流程:
- 小程序端用
uni.chooseImage选择图片,用uni.uploadFile将文件传到我们后端接口。 - 后端接收到文件后,进行简单校验(格式、大小)。
- 调用腾讯云COS的SDK,将文件流上传到指定的Bucket,并设置好访问权限(我们设为公有读私有写)。
- 上传成功后,COS会返回一个文件的访问URL(如
https://your-bucket.cos.ap-shanghai.myqcloud.com/product/202305/abc.jpg)。 - 后端将这个URL返回给前端。前端在提交商品表单时,将图片URL数组一起提交。
- 小程序端用
- 数据库存储:商品表不直接存图片,而是将商品与图片URL的关联关系存在
product_image表中。查询商品详情时,通过product_id联查获取所有图片URL。
实操心得:图片上传一定要做压缩和缩略图。用户手机原图可能好几MB,直接上传和下载体验极差。我们后端的做法是,接收到图片后,使用
Thumbnails等库在内存中生成一个缩略图(例如最长边800像素),将缩略图上传到COS,原图可以选择性上传或丢弃。前端列表页显示缩略图,详情页再加载原图,体验提升巨大。
商品搜索的实现:我们使用了MySQL的LIKE语句进行简单的模糊匹配,并结合了分类和价格筛选。对于更复杂的搜索(如根据商品描述语义搜索),可以考虑引入Elasticsearch,但对于校园级应用,MySQL全文索引(FULLTEXT INDEX)可能就够了,我们这次没用到。
-- 示例查询 SELECT * FROM product WHERE status = 1 -- 上架中 AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) AND category_id = #{categoryId} AND price BETWEEN #{minPrice} AND #{maxPrice} ORDER BY create_time DESC3.4 订单与模拟支付状态机
交易流程的核心是状态管理。我们为订单设计了一个简单的状态机:
待付款 --(用户支付)--> 待发货 待付款 --(超时未支付)--> 已取消 待发货 --(卖家发货)--> 待收货 待收货 --(买家确认收货)--> 已完成 待收货 --(买家申请退款)--> 退款中... (我们项目简化了,没做退款)在service层,每个状态变更的操作(如支付、发货、确认收货)都是一个独立的方法,方法内部会校验当前订单状态是否允许进行此操作,然后更新状态,并可能触发其他动作(如给卖家发微信模板消息通知)。
模拟支付接口:我们创建了一个/api/pay/mock接口,接收订单号。其逻辑非常简单:检查订单是否为“待付款”状态,然后将其更新为“待发货”,并记录支付时间。在实际商业项目中,这里需要调用微信支付统一下单API,处理异步通知等复杂逻辑。
4. 前端uniapp开发核心技巧与避坑指南
4.1 uniapp项目结构与小程序的特殊之处
uniapp的项目结构遵循Vue CLI的风格,但有一些为多端适配而生的特殊目录和文件。
项目根目录/ ├── pages/ // 页面文件,每个页面一个目录,内含.vue文件 ├── static/ // 静态资源,如图标、图片 ├── components/ // 可复用的Vue组件 ├── store/ // Vuex状态管理,我们用来管理用户登录状态、购物车等 ├── common/ // 公共JS,如请求封装、工具函数 ├── uni.scss // 全局SCSS变量 ├── App.vue // 应用入口 ├── main.js // 应用主入口 ├── manifest.json // 应用配置,如小程序AppID、各端特有配置 └── pages.json // 页面路由与窗口样式配置pages.json是关键,它相当于小程序原生开发中的app.json,用于配置页面路径、全局样式、导航栏颜色等。
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "校园二手市场" } }, // ... 其他页面 ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "Uni-App", "navigationBarBackgroundColor": "#F8F8F8" } }4.2 网络请求封装与状态管理
我们使用uni.request进行网络请求,但直接使用非常原始,必须封装。
请求封装(common/request.js):
import store from '@/store/store.js'; // 引入Vuex store const BASE_URL = 'https://your-api-server.com/api'; // 后端API基础地址 const request = (options) => { // 从Vuex获取Token const token = store.state.user.token; return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' // 携带Token }, success: (res) => { // 这里根据后端统一的Result结构处理 if (res.statusCode === 200) { const result = res.data; if (result.code === 200) { resolve(result.data); } else if (result.code === 401) { // Token过期,跳转到登录页 uni.showToast({ title: '登录已过期', icon: 'none' }); store.commit('logout'); uni.navigateTo({ url: '/pages/login/login' }); reject(new Error('未授权')); } else { // 其他业务错误 uni.showToast({ title: result.msg || '请求失败', icon: 'none' }); reject(new Error(result.msg)); } } else { reject(new Error(`网络请求失败,状态码:${res.statusCode}`)); } }, fail: (err) => { uni.showToast({ title: '网络连接失败', icon: 'none' }); reject(err); } }); }); }; // 导出常用的方法 export const get = (url, data) => request({ url, method: 'GET', data }); export const post = (url, data) => request({ url, method: 'POST', data }); // ... put, delete这样,在页面中就可以优雅地调用:import { get, post } from '@/common/request.js';
状态管理(Vuex):我们用一个简单的Vuex store来管理用户全局状态,避免在多个页面间频繁传递openid、token。
// store/store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const store = new Vuex.Store({ state: { userInfo: null, token: null }, mutations: { login(state, payload) { state.userInfo = payload.userInfo; state.token = payload.token; // 可以同时存入本地存储,实现持久化 uni.setStorageSync('userInfo', payload.userInfo); uni.setStorageSync('token', payload.token); }, logout(state) { state.userInfo = null; state.token = null; uni.removeStorageSync('userInfo'); uni.removeStorageSync('token'); } }, actions: { // 可以在这里封装登录的异步action } }); export default store;4.3 页面布局与组件化实践
首页商品列表:我们使用uni-app的<scroll-view>组件实现上拉加载更多。关键在于监听@scrolltolower事件,并配合一个loading状态和pageNo、pageSize参数。
<template> <view> <scroll-view scroll-y @scrolltolower="loadMore" :style="{height: scrollViewHeight + 'px'}"> <product-list :list="productList"></product-list> <view v-if="loading" class="loading-text">加载中...</view> <view v-if="noMore" class="loading-text">没有更多了~</view> </scroll-view> </view> </template> <script> import { getProductList } from '@/api/product.js'; export default { data() { return { productList: [], pageNo: 1, pageSize: 10, loading: false, noMore: false, scrollViewHeight: 0 }; }, onLoad() { this.calcScrollViewHeight(); this.loadData(); }, methods: { calcScrollViewHeight() { // 计算scroll-view高度,需扣除导航栏、tabbar等高度 const sysInfo = uni.getSystemInfoSync(); this.scrollViewHeight = sysInfo.windowHeight - 50; // 50是自定义的顶部搜索框高度 }, async loadData() { if (this.loading || this.noMore) return; this.loading = true; try { const res = await getProductList({ pageNo: this.pageNo, pageSize: this.pageSize }); if (res.length < this.pageSize) { this.noMore = true; } this.productList = [...this.productList, ...res]; this.pageNo++; } catch (err) { console.error(err); } finally { this.loading = false; } }, loadMore() { this.loadData(); } } }; </script>商品发布页的多图上传组件:我们封装了一个ImageUploader组件,处理图片选择、预览、删除,并返回已选的图片临时路径数组给父组件。
<template> <view class="image-uploader"> <view v-for="(item, index) in imageList" :key="index" class="image-item"> <image :src="item" mode="aspectFill" @click="previewImage(index)"></image> <view class="delete-btn" @click="deleteImage(index)">×</view> </view> <view v-if="imageList.length < maxCount" class="add-btn" @click="chooseImage"> <text>+</text> </view> </view> </template> <script> export default { props: { maxCount: { type: Number, default: 9 }, value: { // 用于v-model双向绑定 type: Array, default: () => [] } }, data() { return { imageList: this.value }; }, watch: { value(newVal) { this.imageList = newVal; }, imageList(newVal) { this.$emit('input', newVal); // 同步到父组件 } }, methods: { chooseImage() { uni.chooseImage({ count: this.maxCount - this.imageList.length, sizeType: ['compressed'], // 可以指定压缩图 sourceType: ['album', 'camera'], success: (res) => { // res.tempFilePaths 是临时路径数组 this.imageList = [...this.imageList, ...res.tempFilePaths]; // 这里可以立即调用上传接口,也可以等表单提交时一起上传 // this.uploadImages(res.tempFilePaths); } }); }, deleteImage(index) { this.imageList.splice(index, 1); }, previewImage(index) { uni.previewImage({ current: index, urls: this.imageList }); } } }; </script>4.4 即时通讯功能的简易实现
我们使用了uni.connectSocketAPI建立WebSocket连接。为了简化,连接建立后,客户端发送一个包含token的认证消息到服务器,服务器验证后将该连接与用户ID绑定。
前端核心代码:
// 在App.vue的onLaunch中或用户登录后建立连接 let socketTask = null; function initWebSocket() { const token = uni.getStorageSync('token'); if (!token) return; socketTask = uni.connectSocket({ url: `wss://your-api-server.com/ws?token=${token}`, success: () => { console.log('WebSocket连接成功'); } }); socketTask.onOpen(() => { console.log('WebSocket连接已打开'); }); socketTask.onMessage((res) => { const message = JSON.parse(res.data); // 处理接收到的消息,例如更新Vuex中的聊天记录 store.commit('receiveMessage', message); // 或者显示通知 if (!isChatPageActive(message.senderId)) { uni.showTabBarRedDot({ index: 1 }); // 在消息Tab显示红点 } }); socketTask.onClose(() => { console.log('WebSocket连接关闭'); // 可以尝试重连 setTimeout(initWebSocket, 3000); }); } // 发送消息 function sendMessage(receiverId, content, type='text') { if (socketTask && socketTask.readyState === WebSocket.OPEN) { const msg = { type: 'chat', data: { receiverId, content, msgType: type, timestamp: Date.now() } }; socketTask.send({ data: JSON.stringify(msg) }); } else { uni.showToast({ title: '连接未就绪', icon: 'none' }); } }后端WebSocket处理(使用Spring Boot的WebSocketHandler):我们维护了一个ConcurrentHashMap来管理在线用户和其WebSocketSession的映射。当收到前端消息时,根据receiverId找到对应的session并发送。同时,需要处理连接建立时的认证(通过URL中的token)、连接断开后的清理工作。
重要提醒:这种简单的WebSocket实现仅适用于小规模并发。在真实生产环境,需要考虑使用专业的消息中间件(如Redis Pub/Sub)进行集群间的消息广播,或者直接使用成熟的即时通讯云服务(如腾讯云IM、环信等),它们提供了更完善的消息推送、离线存储、群聊等功能。
5. 前后端联调、部署与上线避坑全记录
5.1 联调环境搭建与常见问题
环境准备:
- 后端:确保本地MySQL、Redis服务已启动。修改
application.yml中的数据库连接信息。直接用IDE(如IntelliJ IDEA)运行CampusTradeApplication的main方法。 - 前端:安装HBuilderX,导入uniapp项目。在
manifest.json中配置小程序的AppID(需要去微信公众平台注册)。 - API对接:修改前端
common/request.js中的BASE_URL为本地后端地址,如http://localhost:8080/api。微信小程序开发工具设置中,勾选“不校验合法域名...”用于本地调试(仅限开发阶段)。
联调常见问题:
- 跨域问题:Spring Boot后端需要配置CORS。我们创建了一个
WebMvcConfig配置类。@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") // 生产环境应指定具体前端域名 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } } - 微信小程序真机预览请求失败:手机和电脑不在同一局域网。需要将后端服务部署到公网可访问的服务器,或者使用内网穿透工具(如ngrok、花生壳)将本地服务暴露到公网,然后将
BASE_URL改为穿透后的地址。 - 图片上传失败:检查Spring Boot的
multipart.max-file-size配置。检查腾讯云COS的SDK配置(SecretId, SecretKey, Region, Bucket)是否正确。权限问题(Bucket的读写权限)。
5.2 微信小程序审核与发布要点
- 内容审核:校园二手交易,要特别注意禁止发布违规商品(如食品、药品、医疗器械、虚拟账号、盗版资源等)。我们后台管理端必须有审核功能,并且在小程序用户协议里明确告知。
- 类目选择:在微信小程序后台设置中,服务类目应选择“电商平台”下的“二手交易”或“生活服务-二手回收”。选错类目可能导致审核不通过。
- 隐私协议:必须在小程序设置中填写《用户隐私保护指引》,特别是如果你收集了用户手机号(用于联系),必须明确说明用途。
- 测试账号:提交审核时,在“版本描述”中提供测试账号和密码,方便审核人员体验所有功能。
- 首次提交很可能被拒:原因可能是“功能不完整”(比如我们的支付是模拟的,但看起来像真的,需要明确提示“此为演示项目,支付功能已模拟”),或者“缺乏内容审核机制”。根据反馈耐心修改即可。
5.3 服务端部署(Linux + Nginx + Jar)
我们选择了一台腾讯云轻量应用服务器(CentOS 7.6)。
部署步骤:
- 环境安装:通过yum安装JDK 8/11、MySQL、Redis、Nginx。
- 数据库初始化:将本地的SQL脚本传到服务器,导入数据库。
- 后端服务部署:
- 在本地用Maven打包:
mvn clean package -DskipTests,会在target目录生成一个campus-trade-0.0.1-SNAPSHOT.jar。 - 将JAR包上传到服务器,例如
/opt/app/目录。 - 编写一个Systemd服务文件(
/etc/systemd/system/campus-trade.service)来管理应用,实现开机自启和日志管理。[Unit] Description=Campus Trade Backend Service After=network.target mysql.service redis.service [Service] Type=simple User=root WorkingDirectory=/opt/app ExecStart=/usr/bin/java -jar campus-trade-0.0.1-SNAPSHOT.jar Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target - 启动服务:
systemctl start campus-trade,systemctl enable campus-trade。
- 在本地用Maven打包:
- Nginx配置:
- 我们使用Nginx作为反向代理,将域名(如
api.yourdomain.com)的请求转发到后端Spring Boot应用(localhost:8080),并配置SSL证书实现HTTPS(小程序要求网络请求必须是HTTPS)。 - 同时,Nginx也用于托管前端H5版本(如果编译了的话)的静态文件。
# /etc/nginx/conf.d/campus-trade-api.conf server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/cert.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } - 我们使用Nginx作为反向代理,将域名(如
- 前端小程序发布:在HBuilderX中点击“发行”->“小程序-微信”,生成代码包,然后用微信开发者工具上传此代码包,提交审核即可。
5.4 性能优化与监控建议
项目上线后,如果用户量增长,需要考虑以下方面:
- 数据库优化:为常用的查询字段(如
product表的category_id,status,create_time)建立索引。定期清理过期数据(如已取消的订单)。 - 缓存策略:使用Redis缓存热点数据,如首页商品列表(设置较短过期时间)、用户信息等。我们用了Spring Cache注解(
@Cacheable,@CacheEvict)来简化缓存操作。 - 图片优化:如前所述,务必使用缩略图。可以考虑使用CDN加速图片访问。
- API响应监控:简单的可以在拦截器里记录每个API的耗时,超过一定阈值(如1秒)就打印警告日志。更专业的可以集成Spring Boot Actuator和Prometheus + Grafana进行监控。
- 日志收集:使用Logback或Log4j2将日志按级别输出到不同文件,并定期归档。可以使用ELK(Elasticsearch, Logstash, Kibana)栈进行集中日志管理。
6. 项目总结与扩展思考
走完这个项目的全流程,相当于把互联网产品一个最小闭环跑了一遍。从需求分析、技术选型、数据库设计、前后端编码、联调测试到部署上线,每个环节都有值得深挖的点。
如果再给我一次机会,我会在哪些地方做得更好?
- 引入更清晰的分层和DDD思想:初期为了快,Service层有些臃肿。后续可以更清晰地划分领域模型,将核心业务逻辑沉淀在Domain层,Service层只负责协调和事务管理。
- 前后端接口契约管理:我们前后端是并行开发的,接口靠口头沟通和Markdown文档,后期有变更容易不同步。下次可以考虑使用Swagger/OpenAPI来自动生成API文档和前端接口调用代码,或者使用YApi这样的接口管理平台。
- 聊天模块的改造:自己实现的WebSocket在功能和稳定性上都有局限。对于核心的沟通功能,应该直接集成成熟的第三方IM SDK,把精力更集中在业务本身。
- 增加更丰富的运营功能:比如数据统计面板(每天发布商品数、成交数)、 banner管理、热门商品推荐算法(简单的基于点击/收藏的排序)、消息推送模板等,让平台更有活力。
这个项目代码虽然只有几千行,但涉及的知识点非常密集。对于学习者而言,我建议不要只停留在“跑起来”,而是可以尝试以下扩展练习:1)将模拟支付替换成真实的微信支付;2)为商品增加评论和评分系统;3)实现一个简单的推荐算法;4)尝试用Docker容器化部署整个应用。每完成一个,你对全栈开发的理解就会更深一层。
本文还有配套的精品资源,点击获取