在构建二手数码产品交易平台时,我选择了SpringBoot+Vue的全栈技术组合。这个选择基于三个核心考量:首先,SpringBoot的约定优于配置特性可以快速搭建后端服务;其次,Vue的组件化开发模式非常适合构建交互复杂的前端页面;最后,这种前后端分离的架构符合当前企业级开发的主流趋势。
后端技术栈具体包含:
前端技术栈则采用:
针对二手交易场景,我设计了8个核心表:
sql复制CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '商品标题',
`category_id` int NOT NULL COMMENT '分类ID',
`brand` varchar(50) NOT NULL COMMENT '品牌',
`price` decimal(10,2) NOT NULL COMMENT '售价',
`original_price` decimal(10,2) DEFAULT NULL COMMENT '原价',
`user_id` bigint NOT NULL COMMENT '发布用户ID',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待审核 1-已上架 2-已售出',
`description` text COMMENT '商品描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
java复制// 密码加密示例
String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword);
商品发布是平台最核心的功能之一,我实现了完整的发布-审核-上架流程:
javascript复制const formData = new FormData();
images.forEach((file) => {
formData.append('files', file.raw);
});
formData.append('title', productForm.title);
// 其他字段...
await axios.post('/api/product', formData);
java复制@PostMapping("/product")
public R uploadProduct(@RequestParam("files") MultipartFile[] files,
@RequestParam String title,
// 其他参数...
) {
// 1. 保存商品基础信息
Product product = new Product();
product.setTitle(title);
// 设置其他字段...
productService.save(product);
// 2. 处理图片上传
List<String> imageUrls = fileService.upload(files);
productImageService.saveBatch(imageUrls.stream()
.map(url -> new ProductImage(product.getId(), url))
.collect(Collectors.toList()));
// 3. 触发审核流程
auditService.submit(product.getId());
return R.ok();
}
交易模块包含三个关键状态转换:
java复制public enum OrderStatus {
UNPAID(0, "待付款"),
PAID(1, "已付款"),
SHIPPED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELLED(4, "已取消"),
REFUNDING(5, "退款中");
// 状态转换校验逻辑
public static boolean canTransfer(OrderStatus from, OrderStatus to) {
// 具体转换规则...
}
}
java复制@Transactional
public void handlePayNotify(String orderNo, BigDecimal amount) {
Order order = orderService.getByOrderNo(orderNo);
if (order.getStatus() != OrderStatus.UNPAID) {
log.warn("订单{}已处理过支付回调", orderNo);
return;
}
// 校验金额
if (order.getAmount().compareTo(amount) != 0) {
throw new BusinessException("支付金额不符");
}
// 更新订单状态
orderService.updateStatus(orderNo, OrderStatus.PAID);
// 通知卖家
messageService.send(order.getSellerId(),
"您的商品" + order.getProductTitle() + "已售出");
}
针对二手交易平台常见的安全风险,我实施了以下防护:
java复制@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
}
}
java复制public class DataMaskingUtil {
public static String maskPhone(String phone) {
if (StringUtils.isBlank(phone)) return "";
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
public static String maskIdCard(String idCard) {
// 类似实现...
}
}
java复制@Cacheable(value = "products", key = "#categoryId+'-'+#page+'-'+#size")
public Page<ProductVO> getProductsByCategory(Integer categoryId, int page, int size) {
return productMapper.selectPage(new Page<>(page, size),
new QueryWrapper<Product>()
.eq("category_id", categoryId)
.eq("status", 1)
.orderByDesc("create_time"));
}
nginx复制# Nginx配置示例
location ~* \.(jpg|png)$ {
add_header Vary Accept;
set $webp "";
if ($http_accept ~* "webp") {
set $webp ".webp";
}
try_files $uri$webp $uri =404;
}
采用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
properties复制# application.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
java复制@Configuration
public class LogbackConfig {
@Bean
public LogstashTcpSocketAppender logstashAppender() {
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.setName("LOGSTASH");
appender.setRemoteHost("logstash-host");
appender.setPort(5044);
appender.setEncoder(new LogstashEncoder());
return appender;
}
}
处理商品库存和订单状态时,我遇到过典型的并发问题。最终采用乐观锁方案:
java复制@Transactional
public boolean placeOrder(Long productId, Integer quantity, Long userId) {
// 1. 检查商品状态
Product product = productMapper.selectById(productId);
if (product.getStatus() != ProductStatus.ON_SALE) {
throw new BusinessException("商品已下架");
}
// 2. 乐观锁更新
int updated = productMapper.updateStock(
productId, quantity, product.getVersion());
if (updated == 0) {
throw new ConcurrentOrderException("库存不足");
}
// 3. 创建订单
Order order = new Order();
// 设置订单信息...
orderMapper.insert(order);
return true;
}
在图片上传功能开发中,总结了以下经验:
javascript复制// 使用compressorjs压缩图片
new Compressor(file, {
quality: 0.6,
maxWidth: 1920,
success(result) {
// 上传压缩后的文件
}
});
java复制public void validateImage(MultipartFile file) {
// 校验文件类型
String contentType = file.getContentType();
if (!Arrays.asList("image/jpeg", "image/png").contains(contentType)) {
throw new IllegalArgumentException("仅支持JPEG/PNG格式");
}
// 校验文件大小
if (file.getSize() > 5 * 1024 * 1024) {
throw new IllegalArgumentException("图片大小不能超过5MB");
}
// 实际校验图片内容
try (InputStream is = file.getInputStream()) {
BufferedImage image = ImageIO.read(is);
if (image == null) {
throw new IllegalArgumentException("无效的图片文件");
}
} catch (IOException e) {
throw new RuntimeException("图片读取失败", e);
}
}
这个二手交易平台从设计到实现共耗时约3个月,期间经历了两次架构调整。最大的收获是认识到良好的领域模型设计比过早优化更重要。特别是在交易系统开发中,最初过度关注性能而忽略了清晰的业务边界划分,导致后期不得不重构部分模块。建议开发类似系统的同行,在前期多花时间理清业务状态流转和领域边界,这能为后续开发节省大量时间