校园二手交易一直是个高频刚需场景。记得我读大学时,每到毕业季宿舍楼下就堆满各种带不走的物品,从教材、台灯到自行车应有尽有。而新生入学时又得花大价钱购置这些物品。这种资源错配现象催生了校园跳蚤市场,但传统线下交易存在三个痛点:
这个SpringBoot+Vue的校园闲置系统正是针对这些痛点设计的数字化解决方案。我在实际部署测试中发现,系统将平均交易周期缩短至1.8天,物品流通率提升到83%。其核心价值体现在:
这个技术栈组合绝非随意选择,而是经过三个维度的考量:
采用改良版JWT流程:
java复制// JWT增强配置示例
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter(){
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
authentication.setDetails(map); // 注入额外用户信息
return authentication;
}
};
converter.setSigningKey("your-secret-key");
return converter;
}
这种设计解决了标准JWT无法实时失效的问题,通过Redis维护了令牌黑名单。
采用Elasticsearch+MySQL双写方案:
json复制{
"mappings": {
"properties": {
"title": {"type": "text","analyzer": "ik_max_word"},
"price": {"type": "double"},
"category": {"type": "keyword"},
"location": {"type": "geo_point"}
}
}
}
实测搜索性能对比:
| 方案 | 平均响应时间 | QPS |
|---|---|---|
| 纯MySQL | 420ms | 58 |
| ES+MySQL | 89ms | 210 |
前端采用Vue+ElementUI实现多图上传组件:
vue复制<template>
<el-upload
action="/api/upload"
list-type="picture-card"
:on-preview="handlePreview"
:before-upload="checkImage"
:limit="9">
<i class="el-icon-plus"></i>
</el-upload>
</template>
<script>
export default {
methods: {
checkImage(file) {
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) this.$message.error('仅支持JPEG格式');
if (!isLt2M) this.$message.error('图片大小需小于2MB');
return isJPG && isLt2M;
}
}
}
</script>
后端使用Spring WebFlux实现非阻塞文件上传:
java复制@PostMapping("/upload")
public Mono<ResponseEntity<String>> upload(@RequestPart("file") FilePart file) {
String filename = UUID.randomUUID() + ".jpg";
Path path = Paths.get(uploadDir, filename);
return file.transferTo(path)
.then(Mono.fromCallable(() -> {
// 生成缩略图
Thumbnails.of(path.toString())
.size(200, 200)
.toFile(path.getParent().resolve("thumb_"+filename));
return ResponseEntity.ok("/uploads/"+filename);
}));
}
订单状态流转采用状态模式实现:
java复制public interface OrderState {
void confirm(OrderContext context);
void cancel(OrderContext context);
void complete(OrderContext context);
}
@Component
@Scope("prototype")
public class PendingState implements OrderState {
@Override
public void confirm(OrderContext context) {
context.setState(ApplicationContext.getBean(ConfirmedState.class));
// 触发短信通知
smsService.send(context.getOrder().getSellerPhone(),
"您的商品已被拍下,请及时确认");
}
}
// 状态上下文
public class OrderContext {
private OrderState currentState;
public void processEvent(String eventType) {
switch(eventType) {
case "confirm": currentState.confirm(this); break;
case "cancel": currentState.cancel(this); break;
case "complete": currentState.complete(this); break;
}
}
}
推荐使用Docker Compose编排:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
volumes:
mysql_data:
关键调优参数:
properties复制spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=30000
spring.redis.lettuce.pool.max-active=16
js复制// vue.config.js
module.exports = {
chainWebpack: config => {
config.optimization.splitChunks({
chunks: 'all',
maxSize: 244 * 1024 // 控制chunk大小
})
}
}
使用JMeter进行100并发测试:
| 接口 | 平均响应时间 | 错误率 | TPS |
|---|---|---|---|
| 商品搜索 | 92ms | 0% | 285 |
| 订单创建 | 153ms | 0.2% | 182 |
| 消息推送 | 68ms | 0% | 320 |
现象:部分安卓手机上传图片报413错误
排查过程:
解决方案:
nginx复制client_max_body_size 5M;
javascript复制beforeUpload(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
if(img.width > 4000 || img.height > 4000) {
reject('图片尺寸过大');
} else {
resolve();
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
现象:限量商品出现超卖
解决方案:
采用Redis分布式锁+乐观锁双重保障:
java复制public boolean createOrder(Long itemId, Integer quantity) {
String lockKey = "lock:item:" + itemId;
String requestId = UUID.randomUUID().toString();
try {
// Redis分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if(!locked) return false;
// 乐观锁更新
int rows = itemMapper.updateStock(
itemId,
quantity,
ItemStatus.ON_SALE.getCode());
return rows > 0;
} finally {
// Lua脚本保证原子性解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId);
}
}
这套系统在实际使用中产生了三个意外价值:
技术演进路线:
在开发过程中最深的体会是:校园场景的系统设计必须考虑"学期脉冲"特性,在开学/毕业季的流量会是平时的5-8倍,这要求架构设计必须预留足够的弹性扩容能力。我们通过HPA(Horizontal Pod Autoscaler)实现了自动扩缩容,平稳度过了最近毕业季的流量高峰。