1. 项目概述与核心价值
这个线上超市购物管理系统是我去年带队为本地连锁超市开发的Java Web解决方案。经过半年实际运营验证,系统日均处理订单量稳定在3000+,高峰期可达8000单,完全替代了超市原有的纸质订单管理模式。系统采用经典的B/S架构,前端使用Bootstrap实现响应式布局,后端基于Spring+SpringMVC+Hibernate(SSH)框架组合,数据库选用MySQL 8.0。
关键设计决策:选择SSH而非Spring Boot主要考虑团队技术栈匹配度。虽然Boot开发效率更高,但团队成员对SSH的AOP事务管理和Hibernate二级缓存机制更熟悉,这对保证初期系统稳定性至关重要。
系统最突出的特点是实现了商品推荐算法与库存预警的智能联动。当用户浏览商品时,基于ItemCF的推荐引擎会实时计算相似商品,同时监控模块会检查推荐商品的库存状态。如果库存低于阈值,不仅会在后台触发预警,前台展示时也会自动添加"库存紧张"标识。这种设计使推荐转化率提升了27%,同时降低了因库存不足导致的订单取消率。
2. 系统架构设计解析
2.1 技术栈选型依据
后端采用分层架构设计:
- 表现层:JSP+Servlet+Filter
- 业务层:Spring 5.3.18(IoC容器管理Bean,AOP处理事务)
- 持久层:Hibernate 5.6.5(二级缓存配置Redis)
- 安全层:Spring Security 5.7.1 + JWT
数据库设计遵循第三范式,主要表包括:
sql复制CREATE TABLE `product` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`category_id` int NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int NOT NULL DEFAULT '0',
`sales` int DEFAULT '0',
`image_url` varchar(255) DEFAULT NULL,
`status` tinyint DEFAULT '1',
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2 核心功能模块设计
2.2.1 商品推荐系统
实现物品协同过滤(ItemCF)算法:
java复制public List<Product> recommendProducts(int productId) {
// 1. 获取同时购买过该商品的用户集合
Set<Integer> userIds = purchaseDao.findUserIdsByProduct(productId);
// 2. 计算商品相似度矩阵(余弦相似度)
Map<Integer, Double> similarityMap = new HashMap<>();
for(Integer uid : userIds) {
List<Product> products = purchaseDao.findProductsByUser(uid);
for(Product p : products) {
if(p.getId() != productId) {
similarityMap.merge(p.getId(), 1.0, Double::sum);
}
}
}
// 3. 按相似度排序返回TopN
return similarityMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(5)
.map(entry -> productDao.findById(entry.getKey()))
.collect(Collectors.toList());
}
2.2.2 库存预警机制
采用观察者模式实现多级预警:
- 黄色预警:库存 < 安全库存(20%)
- 红色预警:库存 < 紧急补货线(5%)
- 自动停售:库存 = 0
3. 关键功能实现细节
3.1 购物车与订单流程
购物车采用Redis缓存设计:
java复制// 添加商品到购物车
public void addToCart(String userId, int productId, int quantity) {
String key = "cart:" + userId;
if(redisTemplate.opsForHash().hasKey(key, productId)) {
redisTemplate.opsForHash().increment(key, productId, quantity);
} else {
redisTemplate.opsForHash().put(key, productId, quantity);
}
// 设置30分钟过期时间
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
}
订单状态机设计:
mermaid复制stateDiagram
[*] --> 待支付
待支付 --> 已取消: 超时未支付
待支付 --> 已支付: 支付成功
已支付 --> 已发货: 仓库处理
已发货 --> 已完成: 用户确认
已发货 --> 退货中: 申请退货
退货中 --> 已退款: 审核通过
3.2 支付系统对接
虽然实际对接了支付宝和微信支付接口,但开发阶段我们使用模拟支付方案:
java复制@Controller
@RequestMapping("/payment")
public class PaymentMockController {
@PostMapping("/mock")
@ResponseBody
public Result mockPay(@RequestParam String orderNo) {
// 模拟支付处理延迟
Thread.sleep(2000);
orderService.updateOrderStatus(orderNo, OrderStatus.PAID);
return Result.success();
}
}
支付安全要点:
- 所有支付请求必须验证签名
- 订单金额以分为单位存储避免浮点误差
- 支付回调接口要做幂等处理
4. 性能优化实践
4.1 数据库优化措施
-
索引优化:
- 商品表建立组合索引(category_id, status, sales)
- 订单表按用户ID分片(user_id % 10)
-
查询优化示例:
java复制// 错误写法:N+1查询问题
List<Order> orders = orderDao.findByUser(userId);
for(Order o : orders) {
List<OrderItem> items = orderItemDao.findByOrder(o.getId());
o.setItems(items);
}
// 正确写法:批量查询
List<Order> orders = orderDao.findByUser(userId);
List<Integer> orderIds = orders.stream().map(Order::getId).collect(Collectors.toList());
Map<Integer, List<OrderItem>> itemMap = orderItemDao.findByOrderIds(orderIds)
.stream().collect(Collectors.groupingBy(OrderItem::getOrderId));
orders.forEach(o -> o.setItems(itemMap.get(o.getId())));
4.2 缓存策略设计
采用多级缓存架构:
- 本地缓存(Caffeine):存储热点商品信息,TTL=5分钟
- 分布式缓存(Redis):存储购物车、会话数据
- CDN缓存:静态资源如图片、CSS/JS文件
缓存击穿解决方案:
java复制public Product getProductWithCache(int id) {
String cacheKey = "product:" + id;
Product product = cache.get(cacheKey);
if(product == null) {
synchronized(this) {
product = cache.get(cacheKey);
if(product == null) {
product = productDao.findById(id);
if(product != null) {
cache.put(cacheKey, product, 5, TimeUnit.MINUTES);
} else {
// 防止缓存穿透
cache.put(cacheKey, Product.EMPTY, 1, TimeUnit.MINUTES);
}
}
}
}
return product == Product.EMPTY ? null : product;
}
5. 安全防护体系
5.1 认证与授权方案
JWT令牌设计:
java复制public String generateToken(User user) {
return Jwts.builder()
.setSubject(user.getUsername())
.claim("roles", user.getRoles())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
// 拦截器验证
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
// 将用户信息存入SecurityContext
} catch (Exception e) {
response.sendError(HttpStatus.UNAUTHORIZED.value());
return false;
}
return true;
}
5.2 常见攻击防护
-
SQL注入防护:
- 全部使用预编译语句
- MyBatis使用#{}占位符
-
XSS防护:
- 前端使用vue-sanitize过滤输入
- 后端对富文本内容使用JSoup白名单过滤
-
CSRF防护:
- 重要操作需验证CSRF Token
- 敏感接口限制为POST方法
6. 部署与监控方案
6.1 容器化部署
Docker Compose配置示例:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql-data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
6.2 监控指标配置
Prometheus监控指标示例:
java复制@RestController
public class MetricsController {
private final Counter orderCounter = Counter.build()
.name("orders_total")
.help("Total number of orders")
.register();
@PostMapping("/order")
public Result createOrder() {
orderCounter.inc();
// 订单创建逻辑
}
}
监控看板重点关注:
- 应用层:QPS、响应时间、错误率
- 数据库:连接数、慢查询、锁等待
- JVM:GC时间、堆内存使用
- 业务指标:订单转化率、支付成功率
7. 开发经验与避坑指南
7.1 典型问题解决方案
问题1:购物车商品价格不一致
- 现象:用户添加商品时显示价格A,结算时变成价格B
- 原因:直接存储商品快照价格,未考虑促销活动更新
- 解决方案:
java复制public CartItem convertToOrderItem(CartItem cartItem) {
Product current = productService.getById(cartItem.getProductId());
OrderItem item = new OrderItem();
item.setProductId(cartItem.getProductId());
// 使用实时价格而非购物车缓存价格
item.setPrice(current.getDiscountPrice());
item.setQuantity(cartItem.getQuantity());
return item;
}
问题2:超卖问题
- 现象:秒杀活动出现库存减为负数
- 解决方案:采用Redis原子操作+数据库乐观锁
java复制public boolean reduceStock(int productId, int quantity) {
String lockKey = "product_lock:" + productId;
// 分布式锁
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if(locked) {
try {
Product product = productDao.findById(productId);
if(product.getStock() >= quantity) {
int affected = productDao.updateStock(productId, product.getStock(), product.getStock()-quantity);
return affected > 0;
}
return false;
} finally {
redisTemplate.delete(lockKey);
}
}
return false;
}
7.2 性能调优经验
-
发现商品分类页响应慢(>2s)
- 问题定位:EXPLAIN分析发现全表扫描
- 优化方案:
- 添加组合索引(category_id, status, sales)
- 引入缓存分类商品列表
- 分页查询优化:使用游标分页替代LIMIT offset
-
订单导出OOM问题
- 现象:导出10万条订单时内存溢出
- 解决方案:
- 改用流式查询
- 分批次处理(每次500条)
- 使用SXSSFWorkbook处理Excel
8. 扩展功能实现
8.1 智能补货预测
基于历史销售数据的补货算法:
java复制public int calculateReplenishment(int productId) {
// 获取过去30天销售数据
List<DailySales> sales = salesDao.findLast30Days(productId);
// 计算日均销量
double avgSales = sales.stream()
.mapToInt(DailySales::getQuantity)
.average()
.orElse(0);
// 考虑安全库存(3天销量)
int safetyStock = (int)Math.ceil(avgSales * 3);
// 当前库存缺口
int currentStock = productDao.getStock(productId);
return Math.max(0, safetyStock - currentStock);
}
8.2 移动端适配方案
针对移动端的特殊处理:
- 图片懒加载
html复制<img data-src="/product/image/123.jpg" class="lazyload">
<script>
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages = document.querySelectorAll("img.lazyload");
var lazyloadThrottleTimeout;
function lazyload() {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazyload');
}
});
}, 20);
}
window.addEventListener('scroll', lazyload);
});
</script>
- 手势操作优化
javascript复制// 商品图片缩放
const img = document.getElementById('product-image');
let initialDistance = 0;
img.addEventListener('touchstart', (e) => {
if(e.touches.length === 2) {
initialDistance = getDistance(e.touches[0], e.touches[1]);
}
});
img.addEventListener('touchmove', (e) => {
if(e.touches.length === 2) {
e.preventDefault();
const currentDistance = getDistance(e.touches[0], e.touches[1]);
const scale = currentDistance / initialDistance;
img.style.transform = `scale(${scale})`;
}
});
function getDistance(touch1, touch2) {
return Math.hypot(
touch2.pageX - touch1.pageX,
touch2.pageY - touch1.pageY
);
}
9. 测试策略与质量保障
9.1 自动化测试体系
测试金字塔实施:
- 单元测试(60%):JUnit 5 + Mockito
java复制@Test
void testCalculateDiscount() {
PricingService service = new PricingService();
// 测试满减规则
assertEquals(90, service.calculateDiscount(100, "FESTIVAL10"));
// 测试无效优惠码
assertEquals(100, service.calculateDiscount(100, "INVALID"));
}
- 集成测试(30%):Spring Boot Test
java复制@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
@Transactional
void testCreateOrder() {
OrderRequest request = new OrderRequest(/* 测试数据 */);
Order order = orderService.createOrder(request);
assertNotNull(order.getId());
assertEquals(OrderStatus.PENDING, order.getStatus());
}
}
- E2E测试(10%):Selenium
java复制public class CheckoutTest {
@Test
public void testGuestCheckout() {
WebDriver driver = new ChromeDriver();
driver.get("https://store.example.com");
// 测试步骤
driver.findElement(By.id("add-to-cart")).click();
driver.findElement(By.id("checkout")).click();
// 断言
assertTrue(driver.getCurrentUrl().contains("checkout"));
driver.quit();
}
}
9.2 性能测试方案
使用JMeter进行压力测试:
-
测试场景设计:
- 模拟1000用户并发浏览商品
- 500用户并发提交订单
- 持续时长30分钟
-
关键监控指标:
- 平均响应时间 < 500ms
- 错误率 < 0.1%
- CPU利用率 < 70%
- 内存使用 < 80%
-
优化前后对比:
- 商品详情页:从1200ms → 350ms
- 订单提交:从800ms → 250ms
- 数据库负载:从75% → 40%
10. 项目演进与反思
10.1 架构演进路线
-
初期(1.0):单体架构
- 所有模块打包为一个WAR
- 优点:部署简单
- 痛点:扩展困难
-
中期(2.0):垂直拆分
- 分离前台/后台服务
- 引入Redis缓存
- 优点:针对性扩展
-
当前(3.0):微服务化
- 商品服务独立部署
- 订单服务独立部署
- 优点:弹性伸缩
10.2 值得改进的设计
-
初期未考虑分库分表
- 后果:订单表单表超过500万条后查询变慢
- 解决方案:按用户ID哈希分片
-
缓存策略不够精细
- 问题:热门商品缓存击穿
- 改进:引入多级缓存+熔断机制
-
监控体系不完善
- 问题:故障发现滞后
- 改进:接入Prometheus+Alertmanager
这个项目让我深刻体会到,好的电商系统不仅要关注功能实现,更需要建立完善的性能监控和应急机制。特别是在大促期间,预先准备的限流降级方案多次避免了系统崩溃。建议开发类似系统的同行,一定要在项目初期就考虑好监控体系和应急预案。