基于SpringBoot+Vue.js的外卖点餐系统全栈开发实战:从零到部署的完整指南

基于SpringBoot+Vue.js的外卖点餐系统全栈开发实战:从零到部署的完整指南
1. 为什么选择SpringBootVue.js开发外卖系统第一次接触外卖系统开发是在2018年当时接了个校园外卖平台的私活。客户要求三个月内上线我硬着头皮用传统的ServletJSP开发结果光是处理高并发请求就差点让我崩溃。后来接触到SpringBoot和Vue.js这套组合开发效率直接提升了三倍不止。SpringBoot最大的优势在于约定优于配置。以前用SSM框架时光是配置XML文件就能写到手软。现在只需要几行代码就能快速搭建起一个可运行的后端服务。我记得第一次用SpringBoot开发用户模块时从创建项目到实现注册登录接口只用了不到半小时。Vue.js则彻底改变了前端开发的体验。以前用jQuery操作DOM就像在迷宫里找路而Vue的响应式数据绑定让前端逻辑变得异常清晰。有次需要紧急修改菜品分类的展示逻辑用Vue只改了数据模型就自动更新了视图这在以前至少要改五六个JS文件。这套技术栈的另一个优势是生态丰富。SpringBoot有Spring Security做权限控制JPA处理数据库操作Vue有Element UI提供现成的美观组件。去年开发的一个外卖平台从零开始到上线只用了两个月这在传统开发模式下简直不可想象。2. 开发环境准备与项目初始化2.1 后端环境搭建我习惯用IntelliJ IDEA作为Java开发工具它的智能提示和代码生成能省不少时间。安装好JDK 1.8后打开Spring Initializrstart.spring.io选上这几个关键依赖Spring Web处理HTTP请求Spring Data JPA数据库操作Lombok简化实体类代码MySQL Driver连接MySQL数据库第一次配置数据库时我踩过坑。application.properties里要这样配置spring.datasource.urljdbc:mysql://localhost:3306/takeaway?useSSLfalseserverTimezoneUTC spring.datasource.usernameroot spring.datasource.password123456 spring.jpa.hibernate.ddl-autoupdate特别注意ddl-auto不要设成create我有次不小心设错了把生产环境的表全清空了...现在我都先用update等表结构稳定后再用Flyway做版本控制。2.2 前端环境配置Vue CLI现在已经是4.x版本了安装时记得用npm install -g vue/cli vue create takeaway-frontend选上Router和Vuex这是现代前端项目的标配。我推荐用Vant作为UI组件库它的移动端适配做得特别好npm install vant -S然后在main.js里引入import Vant from vant import vant/lib/index.css Vue.use(Vant)3. 数据库设计与核心表结构3.1 实体关系设计设计数据库时我习惯先画ER图。外卖系统最核心的五个实体是用户(User)、菜品(Dish)、购物车(Cart)、订单(Order)、订单明细(OrderDetail)。用户和订单是一对多关系订单和订单明细也是一对多。这里有个设计技巧订单明细应该包含下单时的菜品快照信息而不是直接关联菜品表。因为菜品价格可能会变但订单记录必须保持历史数据。3.2 关键表结构示例用户表的SQL定义CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(20) NOT NULL COMMENT 登录账号, password varchar(64) NOT NULL COMMENT 密码, phone varchar(11) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;菜品表的设计要注意支持多规格CREATE TABLE dish ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 菜品名称, category_id bigint NOT NULL COMMENT 分类ID, price decimal(10,2) DEFAULT NULL COMMENT 价格, description varchar(200) DEFAULT NULL COMMENT 描述, status int DEFAULT 1 COMMENT 状态 0停售 1启售, image varchar(255) DEFAULT NULL COMMENT 图片, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;4. 后端核心功能实现4.1 用户认证模块我推荐用JWT做认证比Session更适合前后端分离架构。首先在pom.xml添加依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency然后创建JWT工具类public class JwtUtil { private static final String SECRET_KEY your-secret-key; private static final long EXPIRATION 86400000; // 24小时 public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() EXPIRATION)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } }4.2 菜品管理接口菜品分页查询是个典型场景我通常这样设计ControllerRestController RequestMapping(/dish) public class DishController { Autowired private DishService dishService; GetMapping(/page) public ResultPageDish pageQuery( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String name) { Pageable pageable PageRequest.of(page - 1, size); SpecificationDish spec (root, query, cb) - { ListPredicate predicates new ArrayList(); if (StringUtils.isNotBlank(name)) { predicates.add(cb.like(root.get(name), % name %)); } return cb.and(predicates.toArray(new Predicate[0])); }; return Result.success(dishService.findAll(spec, pageable)); } }Service层使用Spring Data JPA的Specification实现动态查询这是我实践中最灵活的方式。5. 前端页面开发技巧5.1 菜品展示页优化菜品列表要兼顾性能和用户体验。我采用虚拟滚动技术处理大量数据template van-list v-modelloading :finishedfinished finished-text没有更多了 loadonLoad van-card v-foritem in list :keyitem.id :priceitem.price :titleitem.name :thumbitem.image template #footer van-button sizemini clickaddToCart(item)加入购物车/van-button /template /van-card /van-list /template script export default { data() { return { list: [], loading: false, finished: false, page: 1 } }, methods: { async onLoad() { const res await axios.get(/dish/page?page${this.page}) this.list [...this.list, ...res.data.content] this.loading false if (res.data.last) { this.finished true } else { this.page } } } } /script5.2 购物车状态管理购物车状态我用Vuex管理核心代码如下const store new Vuex.Store({ state: { cartItems: JSON.parse(localStorage.getItem(cart)) || [] }, mutations: { addItem(state, dish) { const existing state.cartItems.find(item item.id dish.id) if (existing) { existing.quantity } else { state.cartItems.push({...dish, quantity: 1}) } localStorage.setItem(cart, JSON.stringify(state.cartItems)) }, clearCart(state) { state.cartItems [] localStorage.removeItem(cart) } }, getters: { totalCount: state { return state.cartItems.reduce((sum, item) sum item.quantity, 0) }, totalAmount: state { return state.cartItems.reduce((sum, item) sum (item.price * item.quantity), 0) } } })6. 系统部署实战经验6.1 后端打包与运行用Maven打包时我习惯跳过测试mvn clean package -DskipTests生产环境推荐用Docker部署这是我的Dockerfile模板FROM openjdk:8-jdk-alpine VOLUME /tmp COPY target/takeaway-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]构建并运行容器docker build -t takeaway . docker run -d -p 8080:8080 --name takeaway takeaway6.2 前端部署方案前端项目打包npm run build我推荐用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; } }7. 常见问题排查指南7.1 跨域问题解决开发阶段经常遇到跨域问题我的解决方案是后端配置CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .maxAge(3600); } }前端axios配置baseURLaxios.defaults.baseURL http://localhost:80807.2 性能优化建议数据库查询优化方面我有几个实用技巧给常用查询字段加索引复杂查询使用Query注解优化批量操作使用ModifyingTransactional启用二级缓存例如菜品分类查询可以这样优化public interface CategoryRepository extends JpaRepositoryCategory, Long { Query(SELECT c FROM Category c LEFT JOIN FETCH c.dishes WHERE c.id :id) EntityGraph(attributePaths {dishes}) Category findWithDishesById(Param(id) Long id); }8. 项目扩展与进阶方向8.1 微信小程序集成现在很多外卖平台都要支持小程序。我推荐用uni-app跨平台方案一套代码可以编译到多个平台。小程序登录流程关键代码uni.login({ provider: weixin, success: function (res) { axios.post(/auth/wechat, {code: res.code}).then(response { store.commit(login, response.data) }) } })8.2 支付系统对接支付宝和微信支付对接是必备功能。我的经验是后端使用官方SDK处理签名前端用JSAPI调起支付一定要实现异步通知处理支付回调接口示例PostMapping(/pay/notify) public String payNotify(HttpServletRequest request) { MapString, String params new HashMap(); request.getParameterMap().forEach((k, v) - params.put(k, v[0])); boolean verifyResult AlipaySignature.rsaCheckV1( params, ALIPAY_PUBLIC_KEY, UTF-8, RSA2); if (verifyResult) { String tradeStatus params.get(trade_status); if (TRADE_SUCCESS.equals(tradeStatus)) { String orderNo params.get(out_trade_no); orderService.paySuccess(orderNo); return success; } } return fail; }9. 代码质量与维护建议9.1 单元测试实践我坚持为Service层写单元测试用Mockito模拟依赖ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private OrderRepository orderRepository; InjectMocks private OrderService orderService; Test void shouldCreateOrderSuccessfully() { // given OrderCreateDTO dto new OrderCreateDTO(); // ... 构造测试数据 when(orderRepository.save(any(Order.class))).thenAnswer(invocation - { Order order invocation.getArgument(0); order.setId(1L); return order; }); // when Long orderId orderService.createOrder(dto); // then assertNotNull(orderId); verify(orderRepository, times(1)).save(any(Order.class)); } }9.2 API文档生成用Swagger自动生成API文档配置很简单Configuration EnableSwagger2 public class SwaggerConfig { Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.takeaway)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(外卖系统API文档) .description(前后端接口定义) .version(1.0) .build(); } }10. 项目实战经验分享去年给某高校食堂开发外卖系统时遇到一个典型问题高峰期订单积压。通过分析发现瓶颈在数据库IO于是做了以下优化引入Redis缓存热门菜品数据订单表按月份分表使用消息队列异步处理订单Redis配置示例Configuration EnableCaching public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }然后在Service层添加缓存注解Cacheable(value dishes, key #categoryId) public ListDish findByCategoryId(Long categoryId) { return dishRepository.findByCategoryId(categoryId); }消息队列我用的是RabbitMQ订单创建后发送消息Autowired private RabbitTemplate rabbitTemplate; public void createOrder(OrderCreateDTO dto) { Order order convertToEntity(dto); order orderRepository.save(order); rabbitTemplate.convertAndSend( order.exchange, order.create, order.getId()); }消费者服务异步处理RabbitListener(queues order.queue) public void processOrder(Long orderId) { Order order orderRepository.findById(orderId).orElseThrow(); // 处理订单逻辑... }这些优化使系统在高峰期也能保持稳定订单处理能力提升了5倍。这让我深刻体会到一个好的系统不仅要功能完善更要考虑实际业务场景下的性能表现。