1. 项目概述与技术选型
在校园二手交易平台的开发中,我们采用了SpringBoot+Vue3的全栈技术架构。这个选择基于以下几个关键考量:
-
技术成熟度:SpringBoot作为Java生态中最成熟的微服务框架,提供了完善的开发工具链和社区支持。Vue3作为前端主流框架,其组合式API和响应式系统非常适合构建复杂的单页应用。
-
开发效率:SpringBoot的约定优于配置原则和Vue3的Composition API都能显著减少样板代码,让我们能更专注于业务逻辑实现。
-
性能表现:SpringBoot的内嵌Tomcat和Vue3的虚拟DOM优化都能提供良好的运行时性能,这对交易类平台尤为重要。
技术选型时特别要注意版本兼容性。我们选择Spring Boot 3.x+Java 17的组合,因为从Spring Boot 3.0开始不再支持Java 8,而Java 17是当前的LTS版本。
2. 后端架构设计详解
2.1 分层架构实现
我们采用经典的三层架构设计:
- Controller层:处理HTTP请求和响应,使用
@RestController注解简化RESTful API开发。关键配置示例:
java复制@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public ResponseEntity<List<ProductDTO>> getProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) Double maxPrice) {
// 参数校验逻辑
return ResponseEntity.ok(productService.getProducts(category, maxPrice));
}
}
- Service层:核心业务逻辑实现,使用
@Transactional保证数据一致性。典型服务类结构:
java复制@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
private final RedisTemplate<String, Object> redisTemplate;
@Override
@Transactional(readOnly = true)
public List<ProductDTO> getProducts(String category, Double maxPrice) {
String cacheKey = "products:" + category + ":" + maxPrice;
// 先查缓存
List<ProductDTO> cached = (List<ProductDTO>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
// 缓存未命中则查数据库
List<Product> products = productRepository.findByFilters(category, maxPrice);
List<ProductDTO> result = products.stream().map(this::convertToDTO).toList();
// 写入缓存,设置5分钟过期
redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
}
}
- Repository层:数据持久化,我们采用Spring Data JPA简化数据库操作:
java复制public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE " +
"(:category IS NULL OR p.category = :category) AND " +
"(:maxPrice IS NULL OR p.price <= :maxPrice) AND " +
"p.status = 'ON_SALE'")
List<Product> findByFilters(@Param("category") String category,
@Param("maxPrice") Double maxPrice);
}
2.2 安全认证方案
采用JWT+Spring Security的安全方案:
- Security配置类核心代码:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/products/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
- JWT工具类关键实现:
java复制@Component
@RequiredArgsConstructor
public class JwtService {
private final SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24小时
.signWith(secretKey)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
// 其他工具方法...
}
安全注意事项:生产环境必须使用HTTPS,JWT密钥应当定期轮换,敏感操作需要二次验证。
3. 前端工程化实践
3.1 Vue3项目初始化
使用Vite创建项目并配置关键依赖:
bash复制npm create vite@latest campus-trade --template vue-ts
cd campus-trade
npm install pinia vue-router@4 axios sass
npm install element-plus # 或 naive-ui
核心目录结构:
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── styles/ # 全局样式
├── types/ # TypeScript类型定义
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue # 根组件
└── main.ts # 入口文件
3.2 状态管理设计
使用Pinia管理全局状态,商品模块的Store示例:
typescript复制// stores/product.ts
import { defineStore } from 'pinia'
interface ProductState {
hotProducts: ProductDTO[]
searchResults: ProductDTO[]
currentProduct: ProductDTO | null
}
export const useProductStore = defineStore('product', {
state: (): ProductState => ({
hotProducts: [],
searchResults: [],
currentProduct: null
}),
actions: {
async fetchHotProducts() {
const { data } = await api.get('/api/products/hot')
this.hotProducts = data
},
async searchProducts(keyword: string) {
const { data } = await api.get('/api/products/search', { params: { q: keyword } })
this.searchResults = data
}
},
getters: {
categorizedProducts: (state) => {
return (category: string) =>
state.hotProducts.filter(p => p.category === category)
}
}
})
3.3 路由与权限控制
动态路由配置示例:
typescript复制// router/index.ts
const routes = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
component: () => import('@/views/HomeView.vue'),
meta: { requiresAuth: false }
},
{
path: 'my/products',
component: () => import('@/views/user/MyProducts.vue'),
meta: { requiresAuth: true, roles: ['USER'] }
}
]
}
]
router.beforeEach(async (to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return '/login?redirect=' + encodeURIComponent(to.fullPath)
}
if (to.meta.roles && !authStore.user?.roles.some(r => to.meta.roles.includes(r))) {
return '/403'
}
})
4. 核心功能模块实现
4.1 商品发布流程
- 前端表单实现:
vue复制<template>
<el-form :model="form" :rules="rules" @submit.prevent="submit">
<el-form-item label="商品名称" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number v-model="form.price" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="图片上传">
<el-upload
action="/api/upload"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload">
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-button type="primary" native-type="submit">发布商品</el-button>
</el-form>
</template>
<script setup>
const form = reactive({
title: '',
price: 0,
images: []
})
const submit = async () => {
await api.post('/api/products', form)
// 跳转到商品详情页
}
</script>
- 后端处理逻辑:
java复制@PostMapping
@PreAuthorize("hasRole('USER')")
public ResponseEntity<ProductDTO> createProduct(
@RequestBody @Valid ProductCreateRequest request,
@AuthenticationPrincipal UserDetails user) {
// 验证用户是否被禁言
if (userService.isBanned(user.getUsername())) {
throw new BusinessException("您的账号已被限制发布商品");
}
Product product = productMapper.toEntity(request);
product.setSeller(userService.getByUsername(user.getUsername()));
product.setStatus(ProductStatus.ON_SALE);
Product saved = productRepository.save(product);
return ResponseEntity.created(URI.create("/api/products/" + saved.getId()))
.body(productMapper.toDTO(saved));
}
4.2 交易流程设计
- 订单创建时序:
code复制前端 -> 后端: POST /api/orders (商品ID,数量)
后端 -> 数据库: 创建订单记录
后端 -> Redis: 锁定商品库存
后端 -> 前端: 返回支付信息
前端 -> 支付网关: 发起支付
支付网关 -> 后端: 支付回调
后端 -> 数据库: 更新订单状态
后端 -> Redis: 扣减库存
后端 -> 消息队列: 通知买卖双方
- 库存扣减实现:
java复制@Transactional
public OrderDTO createOrder(OrderCreateRequest request, String username) {
// 使用Redis分布式锁防止超卖
String lockKey = "product_lock:" + request.getProductId();
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) throw new ConcurrentOrderException("操作太频繁,请稍后再试");
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("商品不存在"));
if (product.getStock() < request.getQuantity()) {
throw new BusinessException("库存不足");
}
// 扣减库存
product.setStock(product.getStock() - request.getQuantity());
productRepository.save(product);
// 创建订单
Order order = new Order();
order.setProduct(product);
order.setBuyer(userRepository.findByUsername(username));
order.setStatus(OrderStatus.WAITING_PAYMENT);
// 其他字段设置...
return orderMapper.toDTO(orderRepository.save(order));
} finally {
redisTemplate.delete(lockKey);
}
}
5. 部署与监控方案
5.1 Docker化部署
后端Dockerfile示例:
dockerfile复制FROM eclipse-temurin:17-jdk-jammy
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
前端Dockerfile示例:
dockerfile复制FROM nginx:alpine
COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Nginx配置关键部分:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 监控与告警
Prometheus配置示例:
yaml复制scrape_configs:
- job_name: 'springboot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['backend:8080']
- job_name: 'node'
static_configs:
- targets: ['frontend:9100']
Grafana监控面板应包含:
- JVM内存/线程使用情况
- API请求QPS和延迟
- 数据库连接池状态
- Redis命中率
- 异常请求统计
6. 开发经验与避坑指南
- 跨域问题解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
- 常见性能优化点:
- 使用
@Cacheable注解缓存热点数据 - 数据库查询添加合适索引
- 分页查询必须使用Spring Data的分页对象
- 前端使用懒加载和虚拟滚动优化长列表
- 事务处理注意事项:
java复制// 错误示例:在循环中执行数据库操作
@Transactional
public void batchProcess(List<Long> ids) {
for (Long id : ids) {
repository.updateStatus(id, "PROCESSED"); // 每次循环都会创建新事务
}
}
// 正确做法:一次性提交
@Transactional
public void batchProcess(List<Long> ids) {
repository.updateStatusInBatch(ids, "PROCESSED");
}
- 前端性能监控:
javascript复制// 使用web-vitals监控关键指标
import {getCLS, getFID, getLCP} from 'web-vitals';
function sendToAnalytics(metric) {
console.log(metric);
// 实际项目中发送到监控服务器
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
- 接口文档自动化:
SpringDoc OpenAPI配置:
java复制@Configuration
@OpenAPIDefinition(
info = @Info(
title = "校园二手平台API",
version = "1.0",
description = "API文档"
)
)
public class OpenApiConfig {
@Bean
public OpenAPI customizeOpenAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList("JWT"))
.components(new Components()
.addSecuritySchemes("JWT", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
访问/v3/api-docs获取JSON文档,/swagger-ui.html查看UI界面。