校园二手交易平台是近年来高校信息化建设中的一个重要应用场景。作为一名长期从事校园信息化系统开发的工程师,我观察到每年毕业季和开学季,大量教材、电子产品、生活用品在校园内闲置积压,而传统的QQ群、微信群交易方式存在信息杂乱、交易风险高、难以追溯等问题。
这个基于SpringBoot+Vue的校园闲置物品交易系统,正是为了解决这些痛点而设计的。系统采用前后端分离架构,后端使用Spring Boot提供RESTful API,前端采用Vue.js+ElementUI构建用户界面,数据库选用MySQL,并引入Redis作为缓存层。这种技术栈组合在当前Java Web开发领域非常主流,既保证了系统性能,又便于维护和扩展。
后端技术栈:
前端技术栈:
技术选型心得:在校园环境这种并发量不是特别高的场景下,Spring Boot+MySQL的组合完全够用。Redis的引入主要是为了应对商品列表页的高频访问,将热门商品信息缓存起来,避免频繁查询数据库。
code复制客户端层(Web/Mobile)
↓
表示层(Vue.js + ElementUI)
↓
API网关层(Spring Cloud Gateway)
↓
业务服务层(Spring Boot)
↓
数据访问层(MyBatis-Plus)
↓
数据存储层(MySQL + Redis)
这种分层架构使得系统各模块职责清晰,便于团队协作开发和后期维护。特别是在毕业设计这类需要多人合作的项目中,良好的架构设计能大幅降低沟通成本。
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password_hash` varchar(100) NOT NULL COMMENT '加密密码',
`email` varchar(100) NOT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`school_id` varchar(20) DEFAULT NULL COMMENT '学号/工号',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`last_login` datetime DEFAULT NULL COMMENT '最后登录时间',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-禁用,1-正常)',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
sql复制CREATE TABLE `item` (
`item_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`user_id` bigint NOT NULL COMMENT '发布者ID',
`title` varchar(100) NOT NULL COMMENT '商品标题',
`description` text COMMENT '商品描述',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`original_price` decimal(10,2) DEFAULT NULL COMMENT '原价',
`category_id` int NOT NULL COMMENT '分类ID',
`cover_image` varchar(255) NOT NULL COMMENT '封面图',
`images` text COMMENT '商品图片(JSON数组)',
`publish_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览数',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0-在售,1-已售,2-下架)',
`location` varchar(100) DEFAULT NULL COMMENT '物品位置',
`quality` tinyint DEFAULT NULL COMMENT '成色(1-10)',
PRIMARY KEY (`item_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_category` (`category_id`),
FULLTEXT KEY `ft_title_desc` (`title`,`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
sql复制CREATE TABLE `order` (
`order_id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`buyer_id` bigint NOT NULL COMMENT '买家ID',
`seller_id` bigint NOT NULL COMMENT '卖家ID',
`item_id` bigint NOT NULL COMMENT '商品ID',
`amount` decimal(10,2) NOT NULL COMMENT '交易金额',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`complete_time` datetime DEFAULT NULL COMMENT '完成时间',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0-待支付,1-已支付,2-已完成,3-已取消)',
`payment_method` tinyint DEFAULT NULL COMMENT '支付方式(1-支付宝,2-微信,3-线下)',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`order_id`),
UNIQUE KEY `idx_order_no` (`order_no`),
KEY `idx_buyer` (`buyer_id`),
KEY `idx_seller` (`seller_id`),
KEY `idx_item` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
索引优化:
字段设计:
数据安全:
数据库设计经验:在校园场景下,考虑到学生用户可能频繁更换手机号,不建议将手机号作为唯一标识。同时,商品表的成色(quality)字段采用1-10的评分制,比"全新"、"九成新"等描述更直观。
采用JWT(JSON Web Token)实现无状态认证,解决分布式系统中的会话管理问题。
java复制// JWT工具类核心代码
public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long EXPIRATION_TIME = 86400000; // 24小时
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 其他工具方法...
}
商品模块采用领域驱动设计(DDD)思想,将业务逻辑集中在Service层。
java复制@Service
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
private final ItemMapper itemMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String HOT_ITEMS_KEY = "hot:items";
@Override
@Transactional
public Long publishItem(ItemPublishDTO dto, Long userId) {
Item item = new Item();
BeanUtils.copyProperties(dto, item);
item.setUserId(userId);
item.setStatus(ItemStatus.ON_SALE.getCode());
itemMapper.insert(item);
// 清除缓存
redisTemplate.delete(HOT_ITEMS_KEY);
return item.getItemId();
}
@Override
@Cacheable(value = "items", key = "#itemId")
public ItemVO getItemDetail(Long itemId) {
Item item = itemMapper.selectById(itemId);
if (item == null) {
throw new BusinessException("商品不存在");
}
// 增加浏览量
itemMapper.incrementViewCount(itemId);
return convertToVO(item);
}
@Override
@Cacheable(value = "hotItems")
public List<ItemVO> getHotItems(int limit) {
return itemMapper.selectHotItems(limit).stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
// 其他方法...
}
订单状态机设计:
code复制待支付 → (支付成功) → 已支付 → (确认收货) → 已完成
待支付 → (取消订单) → 已取消
java复制@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final ItemMapper itemMapper;
private final UserMapper userMapper;
@Override
@Transactional
public String createOrder(OrderCreateDTO dto, Long buyerId) {
// 校验商品
Item item = itemMapper.selectById(dto.getItemId());
if (item == null || !ItemStatus.ON_SALE.getCode().equals(item.getStatus())) {
throw new BusinessException("商品不可购买");
}
// 防止自己买自己的商品
if (item.getUserId().equals(buyerId)) {
throw new BusinessException("不能购买自己的商品");
}
// 生成订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setBuyerId(buyerId);
order.setSellerId(item.getUserId());
order.setItemId(item.getItemId());
order.setAmount(item.getPrice());
order.setStatus(OrderStatus.UNPAID.getCode());
orderMapper.insert(order);
// 更新商品状态
itemMapper.updateStatus(item.getItemId(), ItemStatus.PENDING.getCode());
return order.getOrderNo();
}
@Override
@Transactional
public void payOrder(String orderNo, Integer paymentMethod) {
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null || !OrderStatus.UNPAID.getCode().equals(order.getStatus())) {
throw new BusinessException("订单无法支付");
}
// 模拟支付成功
order.setStatus(OrderStatus.PAID.getCode());
order.setPaymentMethod(paymentMethod);
order.setPayTime(new Date());
orderMapper.updateById(order);
// 更新商品状态为已售
itemMapper.updateStatus(order.getItemId(), ItemStatus.SOLD.getCode());
}
// 其他方法...
}
采用Vue3的Composition API实现商品列表展示和筛选功能。
vue复制<template>
<div class="item-list">
<el-row :gutter="20">
<el-col :span="6" v-for="item in items" :key="item.itemId">
<el-card :body-style="{ padding: '0px' }" @click="goDetail(item.itemId)">
<img :src="item.coverImage" class="item-image" />
<div style="padding: 14px;">
<span class="item-title">{{ item.title }}</span>
<div class="item-price">¥{{ item.price }}</div>
<div class="item-meta">
<span>{{ item.schoolName }}</span>
<span>{{ formatTime(item.publishTime) }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-pagination
v-model:currentPage="query.pageNum"
:page-size="query.pageSize"
:total="total"
@current-change="handlePageChange"
layout="prev, pager, next"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getItemList } from '@/api/item'
import { formatTime } from '@/utils/date'
const router = useRouter()
const items = ref([])
const total = ref(0)
const query = ref({
categoryId: null,
keyword: '',
pageNum: 1,
pageSize: 12
})
const fetchItems = async () => {
try {
const res = await getItemList(query.value)
items.value = res.data.list
total.value = res.data.total
} catch (error) {
console.error(error)
}
}
const goDetail = (itemId) => {
router.push(`/item/${itemId}`)
}
const handlePageChange = (page) => {
query.value.pageNum = page
fetchItems()
}
onMounted(() => {
fetchItems()
})
</script>
基于WebSocket实现买卖双方实时通信。
javascript复制// websocket.js
class ChatSocket {
constructor(url, messageHandler) {
this.socket = null
this.url = url
this.messageHandler = messageHandler
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
this.reconnectDelay = 3000
}
connect() {
this.socket = new WebSocket(this.url)
this.socket.onopen = () => {
console.log('WebSocket connected')
this.reconnectAttempts = 0
}
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data)
this.messageHandler(message)
}
this.socket.onclose = () => {
console.log('WebSocket disconnected')
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`)
this.connect()
}, this.reconnectDelay)
}
}
}
send(message) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message))
return true
}
return false
}
close() {
if (this.socket) {
this.socket.close()
}
}
}
export default ChatSocket
推荐使用Docker Compose进行容器化部署,便于环境统一和扩展。
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: campus_trade_mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: campus_trade
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
restart: always
redis:
image: redis:6.2
container_name: campus_trade_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: always
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: campus_trade_backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/campus_trade
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
depends_on:
- mysql
- redis
restart: always
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: campus_trade_frontend
ports:
- "80:80"
restart: always
volumes:
mysql_data:
redis_data:
数据库优化:
缓存策略:
前端优化:
监控告警:
跨域问题:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
nginx复制location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
文件上传:
权限控制:
java复制@PreAuthorize("hasRole('USER') && #userId == authentication.principal.id")
public UserVO getUserProfile(Long userId) {
// ...
}
功能扩展:
技术升级:
安全加固:
项目开发心得:校园二手交易平台虽然业务逻辑不算复杂,但要做一个真正可用的系统,需要考虑很多细节问题。比如商品状态的流转、订单超时处理、聊天消息的存储等。在开发过程中,采用测试驱动开发(TDD)的方式能有效减少Bug,特别是在处理交易流程这种核心业务时。