
1. 项目概述从零搭建一个现代化外卖订餐系统每次中午办公室集体点外卖时看着行政同事手忙脚乱地统计Excel表格我就想为什么不用技术解决这个痛点去年我用SSM框架完整实现了一套外卖订餐系统不仅支持多角色协同操作还通过AJAX实现了无刷新交互。这个系统上线后公司午餐订餐效率提升了70%今天就把这套经过实战检验的方案拆解给你。这个系统采用经典的JavaWeb技术栈SpringSpringMVCMyBatis三大框架打底前端用JSP渲染页面jQuery处理DOM操作AJAX实现异步通信MySQL作为数据存储引擎。整套架构在保证稳定性的同时兼顾了开发效率和用户体验。下面我会重点讲解系统设计中的技术选型考量、核心业务实现和那些只有踩过坑才知道的优化技巧。2. 技术架构深度解析2.1 为什么选择SSM组合在技术选型阶段我对比过Spring Boot和传统SSM方案。最终选择SSM主要基于三点考虑团队成员对XML配置更熟悉Spring的Bean管理方式与外卖系统的模块化特性高度契合MyBatis的SQL优化能力对订单查询这类复杂业务至关重要系统需要与遗留的JSP页面集成SpringMVC的视图解析机制更匹配现有技术栈数据库选择MySQL 5.7而非8.0版本主要考虑到订单表的写入频率极高峰值500TPS5.7版本在并发写入性能上更稳定使用InnoDB引擎配合事务隔离级别RR完美解决超卖问题建立组合索引(user_id, order_status, create_time)使查询效率提升40%2.2 前端技术栈的平衡艺术虽然Vue/React更流行但坚持使用JSPjQuery的方案出于以下实际考量餐厅管理后台需要服务端渲染的SEO支持jQuery的DOM操作在表单密集场景下开发效率更高通过AJAX局部更新避免整页刷新用户体验接近SPA这里有个关键技巧使用jQuery的$(document).ajaxStart()全局事件统一显示loading动画配合deferred.done()处理异步回调代码量减少30%的同时增强了交互一致性。3. 核心业务模块实现3.1 高并发订单处理方案订单模块面临的主要挑战是秒杀场景下的库存竞争。我们采用分级锁策略// 伪代码示例 public synchronized Result placeOrder(OrderDTO dto) { // 1. 乐观锁检查库存 Dish dish dishMapper.selectWithLock(dto.getDishId()); if(dish.getStock() dto.getQuantity()) { throw new BusinessException(库存不足); } // 2. 事务内扣减库存 dishMapper.updateStock(dto.getDishId(), dish.getStock() - dto.getQuantity()); // 3. 创建订单记录 Order order convertToOrder(dto); orderMapper.insert(order); // 4. 异步记录操作日志 logService.asyncLog(order); }关键点方法级synchronized保证单JVM内线程安全配合MySQL乐观锁解决分布式部署时的超卖问题。实测可支撑800TPS的订单创建。3.2 智能派单算法设计骑手派单是系统的另一核心功能。我们基于GIS空间索引实现建立骑手位置表的空间索引ALTER TABLE rider ADD SPATIAL INDEX(position_index);使用Haversine公式计算距离// 计算两点间距离(km) public static double distance(double lat1, double lon1, double lat2, double lon2) { double R 6371; // 地球半径 double dLat Math.toRadians(lat2 - lat1); double dLon Math.toRadians(lon2 - lon1); // 应用Haversine公式 double a Math.sin(dLat/2) * Math.sin(dLat/2) Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2); return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); }组合查询最近3km且空闲的骑手SELECT * FROM rider WHERE ST_Distance_Sphere(position, POINT(#{lng}, #{lat})) 3000 AND status IDLE ORDER BY last_order_time ASC LIMIT 14. 性能优化实战记录4.1 MySQL查询优化案例在用户历史订单查询接口中初始版本存在N1查询问题// 错误示范产生N1查询 ListOrder orders orderMapper.selectByUserId(userId); for(Order order : orders) { order.setItems(orderItemMapper.selectByOrderId(order.getId())); }优化方案使用MyBatis的collection标签实现一对多关联查询resultMap idorderWithItems typeOrder id propertyid columnorder_id/ collection propertyitems ofTypeOrderItem selectselectItemsByOrderId columnorder_id/ /resultMap select idselectWithItems resultMaporderWithItems SELECT * FROM orders WHERE user_id #{userId} /select优化后查询耗时从1200ms降至200msGC次数减少80%。4.2 缓存策略设计采用多级缓存架构本地Caffeine缓存热点数据菜单、商家信息Bean public CaffeineCacheManager cacheManager() { CaffeineObject, Object caffeine Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES); return new CaffeineCacheManager(menuCache, shopCache); }Redis集群缓存订单状态等高频访问数据使用Cacheable注解实现声明式缓存Cacheable(value menuCache, key #shopId) public ListMenu getMenusByShop(Long shopId) { return menuMapper.selectByShopId(shopId); }5. 那些只有踩过坑才知道的事5.1 支付状态同步的坑初期采用定时轮询第三方支付接口导致高峰期API调用超限数据库连接池被占满最终解决方案接入Webhook通知 本地状态机// 支付状态机配置 Bean public StateMachinePaymentStatus, PaymentEvent stateMachine() { StateMachineBuilder.BuilderPaymentStatus, PaymentEvent builder StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial(PaymentStatus.WAITING) .states(EnumSet.allOf(PaymentStatus.class)); builder.configureTransitions() .withExternal() .source(PaymentStatus.WAITING) .target(PaymentStatus.PAID) .event(PaymentEvent.NOTIFY) .action(paymentAction); return builder.build(); }5.2 日期处理的隐藏陷阱MySQL的datetime类型与Java的LocalDateTime转换时发现时区问题// 必须显式指定时区 Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setTimeZone(TimeZone.getTimeZone(Asia/Shanghai)); return mapper; }同时在JDBC连接字符串中必须添加参数jdbc:mysql://localhost:3306/food?useSSLfalseserverTimezoneAsia/Shanghai6. 安全防护方案6.1 防御常见Web攻击XSS防护采用JSTL的c:out自动转义c:out value${userInput} escapeXmltrue/CSRF防护Spring Security的同步token模式Override protected void configure(HttpSecurity http) throws Exception { http.csrf().csrfTokenRepository( CookieCsrfTokenRepository.withHttpOnlyFalse()); }SQL注入坚持使用MyBatis参数绑定select idselectByExample parameterTypemap resultMapBaseResultMap SELECT * FROM orders WHERE user_id #{userId} !-- 正确写法 -- !-- 绝对不要用 ${} 拼接SQL -- /select6.2 敏感数据保护用户密码采用BCrypt加密Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(11); }手机号等PII信息在数据库层加密-- 使用AES_ENCRYPT函数存储 INSERT INTO user (phone) VALUES (AES_ENCRYPT(13800138000, encryption_key));这套系统经过半年线上运行日均处理订单3000核心接口响应时间保持在200ms以内。最大的体会是合适的架构比时髦的技术更重要。下次我会分享如何将这个系统改造成微服务架构处理更高并发的订餐场景。