校园闲置物品交易系统是一个基于现代Web技术构建的线上交易平台,旨在解决高校学生群体中闲置物品流通效率低下的问题。作为一名长期从事校园信息化建设的开发者,我发现学生们每年都会产生大量闲置物品(如教材、电子产品、生活用品等),而传统的线下交易方式存在信息不对称、交易效率低下等痛点。
这个系统采用前后端分离架构,前端使用Vue.js构建响应式用户界面,后端基于Spring Boot框架提供RESTful API服务,数据持久层采用MyBatis+MySQL组合。系统最核心的价值在于:
后端技术栈:
前端技术栈:
系统采用经典的三层架构设计:
code复制表示层(Vue.js)
↓
业务逻辑层(Spring Boot)
↓
数据访问层(MyBatis)
↓
数据存储层(MySQL)
这种分层设计使得各层职责明确,便于维护和扩展。前后端通过定义良好的API接口进行通信,前端专注于用户交互和界面展示,后端处理业务逻辑和数据持久化。
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`real_name` varchar(50) DEFAULT NULL,
`phone_number` varchar(20) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`credit_score` int DEFAULT '100',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_phone` (`phone_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `item` (
`item_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`item_name` varchar(100) NOT NULL,
`item_desc` text,
`price` decimal(10,2) NOT NULL,
`category` varchar(50) NOT NULL,
`publish_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` varchar(20) NOT NULL DEFAULT '待交易',
PRIMARY KEY (`item_id`),
KEY `idx_user` (`user_id`),
KEY `idx_category` (`category`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `order` (
`order_id` bigint NOT NULL AUTO_INCREMENT,
`buyer_id` bigint NOT NULL,
`seller_id` bigint NOT NULL,
`item_id` bigint NOT NULL,
`deal_price` decimal(10,2) NOT NULL,
`order_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`order_status` varchar(20) NOT NULL DEFAULT '待支付',
`buyer_comment` text,
`seller_comment` text,
PRIMARY KEY (`order_id`),
KEY `idx_buyer` (`buyer_id`),
KEY `idx_seller` (`seller_id`),
KEY `idx_item` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
为了提高查询性能,我们在关键字段上建立了索引:
主应用类配置:
java复制@SpringBootApplication
@MapperScan("com.campus.trade.dao")
public class CampusTradeApplication {
public static void main(String[] args) {
SpringApplication.run(CampusTradeApplication.class, args);
}
}
数据库配置(application.yml):
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/campus_trade?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
采用JWT(JSON Web Token)实现无状态认证:
java复制@Component
public class JwtTokenProvider {
private final String secretKey = "your-secret-key";
private final long validityInMilliseconds = 3600000; // 1h
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
// 其他验证方法...
}
核心物品服务包含发布、查询和更新功能:
java复制@Service
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
private final ItemMapper itemMapper;
private final UserMapper userMapper;
@Override
@Transactional
public Item publishItem(ItemDTO itemDTO, Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
Item item = new Item();
BeanUtils.copyProperties(itemDTO, item);
item.setUserId(userId);
item.setStatus("待交易");
itemMapper.insert(item);
return item;
}
@Override
public PageInfo<ItemVO> queryItems(ItemQueryDTO queryDTO, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Item> items = itemMapper.selectByCondition(queryDTO);
PageInfo<Item> pageInfo = new PageInfo<>(items);
return convertToVO(pageInfo);
}
// 其他方法...
}
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── Home.vue # 首页
│ ├── Item/ # 物品相关页面
│ ├── User/ # 用户相关页面
│ └── Order/ # 订单相关页面
└── main.js # 应用入口
vue复制<template>
<div class="item-list">
<el-row :gutter="20">
<el-col
v-for="item in items"
:key="item.id"
:xs="24" :sm="12" :md="8" :lg="6"
>
<item-card :item="item" @click="viewDetail(item.id)"/>
</el-col>
</el-row>
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
@current-change="handlePageChange"
/>
</div>
</template>
<script>
import { getItemList } from '@/api/item'
import ItemCard from '@/components/ItemCard.vue'
export default {
components: { ItemCard },
data() {
return {
items: [],
total: 0,
pageSize: 12,
currentPage: 1
}
},
created() {
this.fetchItems()
},
methods: {
async fetchItems() {
const res = await getItemList({
page: this.currentPage,
size: this.pageSize
})
this.items = res.data.list
this.total = res.data.total
},
handlePageChange(page) {
this.currentPage = page
this.fetchItems()
},
viewDetail(id) {
this.$router.push(`/item/${id}`)
}
}
}
</script>
javascript复制import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: null,
token: localStorage.getItem('token') || ''
},
mutations: {
setUser(state, user) {
state.user = user
},
setToken(state, token) {
state.token = token
localStorage.setItem('token', token)
},
logout(state) {
state.user = null
state.token = ''
localStorage.removeItem('token')
}
},
actions: {
async login({ commit }, { username, password }) {
const res = await login({ username, password })
commit('setToken', res.data.token)
const user = await getCurrentUser()
commit('setUser', user.data)
}
}
})
bash复制# 克隆项目
git clone https://github.com/example/campus-trade.git
# 进入后端目录
cd campus-trade-backend
# 安装依赖
mvn install
# 运行应用
mvn spring-boot:run
bash复制# 进入前端目录
cd campus-trade-frontend
# 安装依赖
npm install
# 启动开发服务器
npm run serve
推荐使用Docker容器化部署:
后端Dockerfile:
dockerfile复制FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/campus-trade-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
前端Dockerfile:
dockerfile复制FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY dist .
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
docker-compose.yml:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: campus_trade
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.0
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
核心功能:
关键实现:
java复制@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@PostMapping("/register")
public Result<UserVO> register(@Valid @RequestBody UserRegisterDTO dto) {
UserVO user = userService.register(dto);
return Result.success(user);
}
@PostMapping("/login")
public Result<String> login(@Valid @RequestBody UserLoginDTO dto) {
String token = userService.login(dto);
return Result.success(token);
}
@GetMapping("/info")
@PreAuthorize("hasRole('USER')")
public Result<UserVO> getCurrentUser() {
Long userId = SecurityUtils.getCurrentUserId();
UserVO user = userService.getUserById(userId);
return Result.success(user);
}
}
核心功能:
关键SQL映射(MyBatis):
xml复制<mapper namespace="com.campus.trade.dao.ItemMapper">
<select id="selectByCondition" resultType="com.campus.trade.entity.Item">
SELECT * FROM item
<where>
<if test="category != null and category != ''">
AND category = #{category}
</if>
<if test="keyword != null and keyword != ''">
AND (item_name LIKE CONCAT('%', #{keyword}, '%')
OR item_desc LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
ORDER BY publish_time DESC
</select>
</mapper>
核心流程:
状态机设计:
code复制待确认 → 已确认 → 已完成
↓
已取消
java复制@PostMapping
public Result createItem(@Valid @RequestBody ItemDTO dto) {
// 只有通过验证的请求才会执行到这里
}
java复制@Cacheable(value = "items", key = "#id")
public Item getItemById(Long id) {
return itemMapper.selectById(id);
}
java复制@Async
public void sendNotification(Long userId, String message) {
// 发送通知的逻辑
}
解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
实现代码:
vue复制<template>
<el-upload
action="/api/upload"
:on-success="handleSuccess"
:before-upload="beforeUpload"
>
<el-button type="primary">点击上传</el-button>
</el-upload>
</template>
<script>
export default {
methods: {
beforeUpload(file) {
const isImage = file.type.startsWith('image/')
if (!isImage) {
this.$message.error('只能上传图片文件')
}
return isImage
},
handleSuccess(res) {
this.$emit('uploaded', res.data.url)
}
}
}
</script>
实现逻辑:
在实际开发过程中,我遇到最棘手的问题是交易状态的并发控制。最初的设计在高并发场景下会出现状态不一致的问题。最终的解决方案是采用乐观锁机制:
java复制@Transactional
public boolean confirmOrder(Long orderId, Long userId) {
Order order = orderMapper.selectById(orderId);
if (!order.getStatus().equals("待确认")) {
throw new BusinessException("订单状态已变更");
}
int rows = orderMapper.updateStatus(orderId, "待确认", "已确认");
if (rows == 0) {
throw new ConcurrentModificationException("订单确认冲突");
}
// 其他业务逻辑...
return true;
}
这个校园闲置物品交易系统从技术选型到功能实现都经过精心设计,特别适合作为毕业设计或学习项目。它不仅包含了现代Web开发的完整技术栈,还涉及了许多实际开发中会遇到的问题和解决方案。