news 2026/9/30 4:17:59

基于B/S架构的美食网站完整实现:从数据库设计到部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于B/S架构的美食网站完整实现:从数据库设计到部署

去年帮一位学弟审毕设,题目就是"基于B/S的美食网站设计与实现"。他最初的设想很轻松:做几个漂亮页面,把菜品图片摆上去,能登录、能下单就算完成。真正动手后才发现,页面反而是最不花时间的部分,评分老师真正看重的是需求是否完整、业务流程是否闭环、数据库有没有合理冗余、项目能不能在一台干净的服务器上跑起来。为了让他少走弯路,我把这个项目的完整实现过程整理成了一套可复现的方案,配套需求文档、数据库设计文档、部署文档和分层源码全部对应。这篇文章就是从需求分析开始,到表结构、核心模块、前端交互,再到服务器部署和实测踩坑的完整复盘。如果你正在找一套可以照着做的美食网站源码和配套文档,照着这个思路走,能省掉不少弯路。

1. 美食网站项目的需求画像:不是"做个页面"那么简单

1.1 这类项目真正的评分点在哪里

我见过很多来问"美食网站怎么做"的同学,第一版方案几乎都是照着外卖App画页面。等到答辩的时候,老师问三个问题就露馅了:购物车下单以后库存怎么减?订单状态怎么流转?管理员能不能下架一个已经被人下单的菜品?这三个问题,任何一个没想清楚,代码写得再漂亮也拿不到高分。

课程设计或者毕业设计的评分逻辑,其实和公司里的项目验收很像。页面美观度只是第一档,真正拉开差距的是三个维度:功能完整性、业务闭环、数据库设计合理性。所谓功能完整性,不是说你有十个页面,而是前台从注册登录到下单评论,后台从菜品维护到订单处理,两条业务线都能走通;业务闭环则体现在库存、订单、支付状态这些数据能互相咬合;数据库设计合理性,看的是有没有冗余字段、有没有该建的索引、有没有把状态字段设计成能支撑业务流转的整形状态机。

所以做这个项目前,我先把需求文件写成了用例清单。这份文档不是摆设,后面每一张表、每一个Controller都能对应上,答辩的时候老师指着需求文档问"这个功能在哪",三分钟就能定位到代码和SQL脚本。

1.2 前台展示与后台管理的功能清单

美食网站表面上是一个展示平台,拆开看其实是两条业务主线:前台是食客的浏览和交易流程,后台是管理员的运营和维护流程。我最初整理的功能清单如下。

端功能模块说明
前台用户注册登录用户名密码注册,BCrypt加密存储,登录后Session保存会话
前台菜品分类浏览按川菜、粤菜、家常菜、甜点饮品等分类筛选
前台菜品搜索支持菜名关键字模糊查询,分页展示
前台菜品详情展示图片、价格、库存、描述,以及用户评论列表
前台购物车加入、修改数量、删除、清空,实时合计金额
前台下单结算填写收货人信息,生成订单并扣减库存
前台我的订单按状态查看订单,支持取消待支付订单
前台评论与收藏已登录用户可对菜品评论,可收藏喜欢的菜品
后台分类管理新增、编辑、删除菜品分类
后台菜品管理菜品CRUD、图片上传、上下架切换
后台订单管理查看所有订单,发货、完成、取消操作
后台用户管理查看用户列表,禁用异常账号
后台数据概览统计用户数、菜品数、订单数、销售额

非功能需求容易被忽略,但也很关键。我要求页面兼容Chrome和Edge,移动端至少能正常浏览,所有表单都做前端校验和后端校验,密码不能明文入库,图片上传要做类型和大小限制。这些点不需要写多少代码,但文档里写上、代码里落实,评语里就会多一句"考虑得比较全面"。

2. 技术选型:为什么B/S架构是这类项目的最优解

2.1 B/S架构的本质与三层结构

先明确一个最基本的概念。B/S架构,就是Browser/Server架构,用户不需要安装任何客户端,只要打开浏览器输入网址,就能访问服务器上部署的应用。美食网站面向的人群是分散的食客,如果用传统C/S(Client/Server)架构,意味着每个用户都要下载安装包,升级一次还要全网重新安装,这在互联网场景下是不可接受的。

B/S架构天然分成了三层:表现层就是浏览器里看到的页面,业务逻辑层处理注册、下单、库存判断这些规则,数据访问层负责和数据库打交道。分层最大的好处是改起来不牵连。比如我想把菜品搜索从模糊查询升级成全文检索,只需要改Service层和对应的Mapper,页面和数据库连接都不用动;再比如前台页面从Thymeleaf换成Vue,只要Controller把数据返回成JSON,表现层整体替换,业务层和数据层可以原封不动。

我做这个项目的时候,把代码分层固定成了controller、service、mapper、entity四层。Controller只做参数接收和视图转发,Service写业务逻辑,Mapper写SQL,Entity对应数据库表。很多课设代码把业务逻辑写在Controller里,一个方法几百行,看着省事,后面调试和加功能的时候就知道苦头了。

2.2 技术栈怎么选:Java、Python还是PHP

技术栈选择要结合"这是课程设计/毕设"这个场景来考虑。不是越新越好,也不是越简单越好,而是资料多、好验证、能说清楚原理最好。我做了个对比表格,纯个人经验。

技术栈优点缺点适合场景
Spring Boot + MyBatis-Plus + MySQL生态成熟,中文资料极多,结构化好启动重,概念多课设/毕设首选,答辩好讲
Flask / Django + SQLite/MySQL轻量,上手快,代码量小大型项目规范需要自己把控偏Python方向的项目
PHP + MySQL部署简单,老项目多工程化体验一般快速原型
Node.js + Express + MongoDB前后端统一JS,开发快事务和关系型数据场景不如MySQL直观偏后端的Web方向

我最终选的是Spring Boot 3 + MyBatis-Plus + MySQL 8 + Thymeleaf + Bootstrap 5。理由是这套组合非常适合"B/S架构+美食网站"这个题目:Spring Boot让配置变得极简,MyBatis-Plus原生支持分页插件和Lambda条件构造器,省掉大量手写SQL;Thymeleaf做服务端页面渲染,正好符合B/S架构"服务端生成页面"的教学点;Bootstrap 5可以快速把后台管理页面做得像模像样,不需要花太多时间在前端调样式上。

这里说明一下,如果你更熟悉Python,用Flask或Django搭出来的系统结构完全一样:路由对应Controller,ORM模型对应Entity,模板对应前端页面。后面的部署部分我也会给出Python项目的启动方式,思路是互通的。

3. 数据库设计:菜品、分类、订单与评论的表结构落地

3.1 核心表结构设计

数据库是这类项目的命根子。表设计得好,Service层写起来像填空;表设计得烂,后面每个查询都是地狱。我最终定了七张核心表:sys_user(用户表)、category(菜品分类表)、dish(菜品表)、cart(购物车表)、orders(订单表)、order_item(订单明细表)、comment(评论表)。收藏功能我单独建了一张favorite表,避免在用户表里堆一个逗号分隔的字段。

先看用户表和菜品表,这两个是最基础的实体。

CREATE TABLE `sys_user` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `nickname` VARCHAR(50), `phone` VARCHAR(20), `avatar` VARCHAR(255), `role` TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', `status` TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `dish` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `category_id` INT NOT NULL, `name` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `image` VARCHAR(255), `status` TINYINT DEFAULT 1 COMMENT '1-在售 0-下架', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_category` (`category_id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

特别注意把金额字段设计成DECIMAL(10,2)而不是FLOAT或DOUBLE。浮点数在累加运算时会有精度误差,订单金额算错了是要出事的。菜品库存字段用INT,下单扣库存必须放在事务里,防止并发情况下库存变负数,这个在第4章展开。

订单表和订单明细表是核心中的核心。

CREATE TABLE `orders` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `order_no` VARCHAR(32) NOT NULL UNIQUE, `user_id` INT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-待付款 1-待发货 2-待收货 3-已完成 4-已取消', `receiver_name` VARCHAR(50), `receiver_phone` VARCHAR(20), `receiver_address` VARCHAR(255), `remark` VARCHAR(255), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_user` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `order_id` INT NOT NULL, `dish_id` INT NOT NULL, `dish_name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `quantity` INT NOT NULL, KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么订单表里不直接存一份购物车JSON?为什么order_item要单独一张表?原因有两个。第一,一个订单包含多个菜品,一张表存不下这种一对多关系;第二,下单之后菜品可能改名、改价格,订单明细里如果不做快照,用户回看历史订单时看到的可能就是另一个价格了。所以我在order_item表里冗余了dish_name和price字段,这叫历史快照,是订单系统设计里很基本的思维。

3.2 外键、索引与初始化数据

我没在表之间建物理外键约束,只保留了逻辑外键。原因很实际:MyBatis-Plus做连表查询时,物理外键还会限制删除顺序,给课程设计增加不必要的麻烦。逻辑外键的意思是,dish.category_id指向category.id这种关系由代码去保证,数据库层面不设FOREIGN KEY。这样删数据灵活,也不影响你写SQL时用JOIN。

索引设计上面已经体现了:dish表的category_id和name,orders表的user_id和status,order_item表的order_id。这些索引不是乱加的,都是实际查询里WHERE和ORDER BY经常用到的字段。比如后台按状态查订单、前台按分类查菜品,没有索引的表数据量一大就会全表扫描。

初始化数据我准备了一个food.sql,里面包含一个管理员账号(用户名admin、密码加密后存储)、几个默认分类,以及二十道左右的示例菜品。这里有个易错点:管理员密码不能像普通数据一样用明文插入,必须是BCrypt加密后的字符串,否则注册时加密、登录时比对的行为不一致,管理端永远登不进去。初始化数据不要贪多,够演示和测试就行,重点是每个菜品的分类ID要和category表对得上。

订单状态字段我用的是TINYINT数字状态机。0到4分别代表待付款、待发货、待收货、已完成、已取消。这样的好处是数据库体积小、查询快,前端再用th:switch把数字映射成中文标签显示。比直接存字符串"待发货"要规范,也比搞一套完整的工作流引擎要简单得多,对课设来说刚好踩在正确的位置上。

4. 核心模块实现:从登录到购物车的完整链路

4.1 注册登录与权限控制

用户模块看起来简单,但安全细节不能省。密码我用的BCrypt加密,不是MD5也不是SHA。BCrypt每加密一次会生成随机的盐,同密码两次加密结果不同,数据库中即使被拖库,反查概率也很低。Spring Security里可以直接用BCryptPasswordEncoder,不加整个Security框架也行,单独引一个spring-security-crypto依赖就够了。

登录成功之后,我把用户对象放进Session。为什么不用JWT?因为这个项目是服务端渲染的B/S架构,页面跳转靠Session天然合理,服务端可以随时让会话失效。JWT更适合前后端分离、移动端App那种场景,放在这里徒增复杂度,答辩时还要花精力解释"为什么用JWT不用Session",纯属自己给自己挖坑。

权限控制我写了一个拦截器,拦截除登录页、注册页、菜品浏览页之外的所有路径。管理员路径/admin/**额外校验role字段,不是管理员直接返回403。

public class AuthInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } if (request.getRequestURI().startsWith("/admin/") && !Integer.valueOf(1).equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }

这里有个新手常犯的错:user.getRole()如果是Integer类型,用==比较的是引用地址,哪怕值相同也可能为false。所以要用Integer.valueOf(1).equals(...)或者把角色字段定义成int基本类型。我见过不下三次这种Bug,页面权限莫名失效,最后都是栽在包装类型比较上。

4.2 菜品分类、分页与搜索

菜品列表页是前台访问量最大的页面。我用MyBatis-Plus的分页插件配合Lambda条件构造器,一次性解决分类筛选、关键字搜索、分页三个问题。

@GetMapping("/dish/list") public String list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "12") Integer size, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String keyword, Model model) { Page<Dish> p = new Page<>(page, size); LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(categoryId != null, Dish::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Dish::getName, keyword) .eq(Dish::getStatus, 1); dishService.page(p, wrapper); model.addAttribute("page", p); return "dish/list"; }

这段代码里容易忽略的是前台的菜品查询永远要带status = 1条件。不然管理员下架的菜品照样出现在食客端,逻辑上就穿帮了。后台菜品管理页面查菜时则要覆盖所有状态,所以后台的查询接口需要单独写,不能直接复用前台的Service方法。

分页插件需要在配置类里注册MybatisPlusInterceptor并添加PaginationInnerInterceptor,这一步漏掉的话,Page参数会被当成普通数据传入,查出来的数据不分页,这是MyBatis-Plus最经典的配置问题之一。

4.3 购物车与下单的事务处理

下单是整个项目最考验逻辑的部分。购物车表cart只存user_id、dish_id、quantity,真正下单的时候不能只把购物车数据扔给订单表就完事,必须重新查一次菜品价格和库存。

下单流程我设计成五步:第一步,遍历购物车,查出每个菜品的当前价格和库存;第二步,校验菜品是否在售、库存是否够;第三步,创建订单主记录,算总金额;第四步,批量插入订单明细,同时扣减库存;第五步,清空购物车。这五步必须在一个事务里完成,任何一步失败,前面的插入和扣减都要回滚。

@Transactional public Order createOrder(Long userId, List<CartItem> items, OrderAddress address) { BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { Dish dish = dishMapper.selectById(item.getDishId()); if (dish == null || dish.getStatus() != 1) { throw new BusinessException("菜品已下架:" + item.getDishId()); } if (dish.getStock() < item.getQuantity()) { throw new BusinessException("库存不足:" + dish.getName()); } total = total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); order.setReceiverInfo(address); orderMapper.insert(order); for (CartItem item : items) { Dish dish = dishMapper.selectById(item.getDishId()); OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(dish.getId()); oi.setDishName(dish.getName()); oi.setPrice(dish.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); // 注意:扣库存语句本身要带 stock >= quantity 条件,防止超卖 int rows = dishMapper.decreaseStock(dish.getId(), item.getQuantity()); if (rows == 0) { throw new BusinessException("库存不足:" + dish.getName()); } } cartMapper.clearByUserId(userId); return order; }

这里有一个很多人会踩的坑:@Transactional默认情况下,只有RuntimeException才会触发回滚。如果我在校验失败时抛的是普通的Exception,Spring不会回滚,前面插入的订单记录就会残留在数据库里。所以业务异常要么继承RuntimeException,要么在@Transactional上显式声明rollbackFor = Exception.class。

另外一个并发细节是decreaseStock方法。最稳妥的做法不是先SELECT stock再UPDATE,而是直接用一条带条件的更新语句:

UPDATE dish SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}

如果更新影响行数为0,说明库存不够,立即抛异常。这样可以避免两个用户同时抢最后一份菜品时都读到库存够、然后都扣减成功的问题。这也是我第一次做的时候没注意,后来实测用两个浏览器同时下单才暴露出来的Bug。

4.4 评论与收藏模块

评论和收藏是两个相对独立的小功能,但设计上要注意关联关系。评论表comment记录了user_id和dish_id,在下单完成后才允许评论吗?我采取的是宽松策略:只要用户登录且购买了该菜品,就可以评论。代码里会校验订单里是否包含这个菜品,防止没买过的人乱刷评论。

收藏功能用favorite表,核心是给user_id和dish_id加唯一约束,防止重复收藏。页面上的"收藏"按钮要做成可切换状态:已经收藏显示红心,点击后取消收藏;没收藏显示空心,点击后写入收藏。这个状态的判断,在菜品详情页渲染时通过当前用户ID去收藏表里查一次即可,数据量不大,效率上没问题。

5. 前端页面与交互:Bootstrap + Thymeleaf 的快速实现

5.1 前台页面结构与交互

前端我用Thymeleaf做服务端渲染,没有单独拆Vue工程。原因很直接:这个项目的核心是B/S架构的教学演示,服务端生成页面本身就是B/S的一部分,用Thymeleaf可以让Controller返回数据后直接在模板里循环渲染,不用额外处理跨域、不用写一堆Ajax调用。一套Vue分离项目做下来,前端代码量比后端还大,课设的时间成本会失控。

页面布局用Bootstrap 5的栅格系统。首页是导航栏加轮播图,下面是分类标签和菜品卡片列表。一个简单的菜品卡片模板:

<div class="col" th:each="dish : ${page.records}"> <div class="card h-100"> <img th:src="${dish.image}" class="card-img-top" alt="菜品图片"> <div class="card-body"> <h5 class="card-title" th:text="${dish.name}">菜名</h5> <p class="card-text" th:text="${dish.description}">菜品描述</p> <span class="text-danger fw-bold" th:text="'¥' + ${dish.price}">价格</span> <a th:href="@{/dish/detail/{id}(id=${dish.id})}" class="btn btn-primary btn-sm float-end">查看详情</a> </div> </div> </div>

这里有个实用的细节:菜品图片如果没有上传,数据库里存的是空字符串,前端渲染的<img>就是个裂图。我在页面上加了th:if="${dish.image != null && dish.image != ''}"判断,没有图片就显示一张默认图,这种小细节在演示的时候观感差别很大。

菜品详情页除了基础信息,还会显示评论列表。我直接复用分页组件,评论列表单独分页。详情页里"加入购物车"按钮需要用户登录才能操作,未登录时点击会跳转登录页,这个逻辑写在Controller里,同时也在页面上用th:if判断当前Session是否为空来切换按钮文案。

购物车页面是一个表格,每行有菜品名、单价、数量输入框、小计和删除按钮。数量修改用一个简单的"加减按钮"配上隐藏的菜品ID,点击后提交到后端更新购物车。购物车的合计金额我用JavaScript在前端实时计算,后端在下单时重新计算一次,以第二次为准,防止用户篡改页面金额。

5.2 后台管理页面与权限隔离

后台页面我放在了/admin/**路径下,和前台完全隔离。管理员登录后,导航栏会多出"后台管理"入口,这个入口同样用th:if判断当前用户角色,普通用户看不到。

后台管理页面主要三个大块:仪表盘、菜品管理、订单管理。仪表盘我做了四个统计卡片:用户总数、菜品总数、订单总数、营收总额,数据通过一个DashboardController查询后填充。菜品管理是一个表格,每行有图片缩略图、名称、分类、价格、库存、状态,操作按钮包含编辑、上下架、删除。图片上传我用MultipartFile接文件,保存到服务器本地的/uploads目录,数据库只存相对路径。

订单管理表格的每一行会根据订单状态显示不同的操作按钮。待发货可以点"发货",待收货可以点"完成",待付款可以点"取消"。这些状态流转按钮背后就是更新orders表的status字段,同时往操作记录里写一条日志。为了演示方便,我没有做完整的操作日志表,而是在代码里用System.out打印关键操作,答辩时看控制台输出就能讲清楚流程。

5.3 响应式适配与表单校验

Bootstrap的栅格系统天然支持响应式。桌面端卡片一行显示四列,平板两列,手机上单列。关键是把col-lg-3 col-md-4 col-sm-6 col-12这些类加到卡片外层容器上,页面不需要额外写媒体查询。

表单校验我做了前后端两层。前端用Bootstrap自带的required、maxlength这些属性做基础校验,比如手机号格式、库存数字范围。后端在Controller里用@Valid配合参数校验注解,Spring Boot 3里可以直接用jakarta.validation.constraints包下的注解。有一点必须提醒:前端校验永远只是体验优化,后端校验才是安全底线。因为请求可以被绕过浏览器直接拿Postman发,如果后端不校验,非法数据就会进数据库。

6. 部署到服务器的完整流程:从本地跑通到外网可访问

6.1 环境准备与打包

很多人做课设习惯只在本地IDEA里跑通,答辩前一周才发现服务器上跑不起来,那是很被动的。部署这件事,至少要在提交前完整走一遍。我的部署环境用的是最低配的云服务器,2核2G就够,系统是Ubuntu 20.04。需要安装的东西就三样:JDK 17、MySQL 8、以及打包用的Maven(也可以本地打包好再把jar传上去,服务器上不装Maven)。

本地先执行打包命令:

mvn clean package -DskipTests

打包完成后,在target目录下会生成food-sys-1.0.0.jar。这里有个细节:打包前一定要把测试类的@SpringBootTest注释掉或者跳过,不然打包时会尝试连接本地数据库,连不上直接构建失败。用-DskipTests只是跳过测试执行,比注释测试类靠谱。

上传jar包可以用scp命令,也可以直接用宝塔面板的文件管理器拖拽上传,传到一个固定目录,比如/www/app/。Windows和Linux之间的文件传输没有太多坑,唯一要注意的是配置文件别用记事本编辑后直接上传,UTF-8编码容易变成带BOM头,Spring Boot读取配置可能报错。

6.2 数据库导入与配置修改

服务器上数据库需要手动创建,注意字符集。直接执行:

CREATE DATABASE food CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

然后导入SQL文件:

mysql -uroot -p food < food.sql

接着修改application-prod.yml,把数据库地址从localhost:3306改成服务器的内网地址或者直接保持localhost,数据库账号密码换成服务器上实际的账号密码。另外要改两处地方:

一是MySQL连接字符串里的时区参数,建议写serverTimezone=Asia/Shanghai,不然数据库和Java程序之间容易出现8小时的时差,导致订单创建时间显示成凌晨。二是文件上传路径,本地开发我用的D:/upload/,Linux上要改成绝对路径/www/uploads/,并且确保目录存在、运行Java程序的用户有写权限。

6.3 启动、防火墙与反向代理

启动命令我用的是nohup加后台运行:

nohup java -Xms256m -Xmx512m -jar /www/app/food-sys-1.0.0.jar --spring.profiles.active=prod > /www/app/nohup.out 2>&1 &

-Xms256m -Xmx512m是限制JVM的堆内存。2G内存的服务器如果不限制堆,Spring Boot默认可能吃掉一大半,系统别的进程就会很卡。启动后立刻看一下日志:

tail -f /www/app/nohup.out

看到"Started FoodSysApplication"基本就成功了。如果端口没起,用netstat -tlnp | grep 8080确认。

让外网用户通过80端口访问,最省事的方式是装一个Nginx做反向代理。配置核心片段如下:

server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /www/uploads/; } }

如果你用的是Python Flask实现,部署逻辑大体一致,只是启动进程从java -jar换成Gunicorn:

pip install gunicorn nohup gunicorn -w 4 -b 0.0.0.0:8080 app:app > gunicorn.log 2>&1 &

最后不要忘了在云服务器的安全组里放行SSH端口、80端口和443端口。如果配置了Nginx,8080端口只需要在内网监听,不用对公网开放,这样更安全。

6.4 几个部署侧边问题

部署阶段还有一个常见问题:jar包更新了但进程还是旧版。原因是没杀掉旧进程就重新启动,8080端口被占用,新进程起不来。这时候先找进程再杀:

ps -ef | grep food-sys.jar kill -9 进程号

另外一个容易被忽略的是上传目录权限。Java进程如果是一个专门的服务用户启动的,而上传目录是root创建的,写入就会失败。我会把/www/uploads的属主改成启动进程的用户,改完再验证一次上传功能。

7. 实测踩坑记录:这些细节文档里不会写

7.1 图片裂图与保存路径的坑

我最初把上传的图片保存到项目源码的static/uploads目录下,本地运行一切正常,打包部署后访问图片全部裂图。原因很简单:jar包运行起来以后,项目内部路径是只读的临时目录,不可能往里写文件。正确做法是把上传目录独立出来,比如Linux下放/www/uploads,Nginx配置一个/uploads/别名指向它,数据库里存/uploads/xx.jpg这种相对路径,页面直接拼接访问。这个问题我花了半天才排查清楚,写下来希望后面的人直接避开。

7.2 MySQL 8驱动名和时区问题

我用的MySQL 8驱动类名是com.mysql.cj.jdbc.Driver,不少旧教程还在用com.mysql.jdbc.Driver,直接启动报错。连接字符串里的serverTimezone=Asia/Shanghai也不能省,否则时间字段整体偏8个小时。这个问题在本地开发时往往发现不了,因为开发机和数据库在同一时区,部署到云服务器后就会冒出来。

7.3 Session失效与事务不回滚

有两个问题我是在演示当天才发现的。第一个是Session超时时间默认30分钟,演示时讲解拖长了,回头刷新页面发现登录态没了,站在台上很尴尬。我最后把Session超时时间调整到了2小时,虽然不推荐生产环境这么做,但课设演示确实有需要。

第二个是@Transactional失效问题。我在同一个类里写了一个createOrder方法,在里面直接调用this.sendSms方法,结果sendSms抛异常,订单数据也没回滚。原因是@Transactional是基于代理实现的,同一个类内部的this调用不会经过代理对象,注解就失效了。解决方式是拆分Bean,或者把需要事务保护的方法单独放到一个Service类里,从外部注入调用。

7.4 Windows本地与Linux服务器的路径分隔差异

本地开发是Windows,服务器是Linux,最容易踩的就是路径。我写文件保存路径时一开始用了File.separator,这只能保证平台自适应,但配置文件里的路径分隔符还是要注意。比如Linux的绝对路径是/www/uploads/,Windows里如果配成D:\upload\,反斜杠在配置里需要转义。更省心的做法是配置里统一用正斜杠/,Java在Windows上也能识别/分隔符,不会出问题。

7.5 懒加载与JSON序列化

这个坑是我后期扩展接口时遇到的:菜品和分类之间有关联关系,我图省事在实体类里加了一个Category category字段,查询时用@ManyToOne懒加载。结果在前端渲染或者返回JSON的时候,Jackson序列化触发懒加载,一旦Session关闭就报LazyInitializationException。这个问题的常规解法是用@JsonIgnore忽略不需要序列化的关联字段,或者把关联查询做成显式DTO,只返回必要字段。课程设计里不要为了省事把关联对象直接序列化给前端,极容易踩坑。

整体做完这个项目,我最大的体会是:代码反而是最不值钱的部分,真正花时间的是把业务逻辑想清楚、把表设计好、把部署验证完。如果你照着这个思路去复现,哪怕不用我这套源码,自己用Flask或者Django搭一套,流程和坑基本也都是这些。最后再提一个小建议:拿到源码先别急着跑,把food.sql导入数据库后,先把配置文件的账号密码改了,再启动项目,否则排查问题时会混入环境因素。祝一次通过。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 4:17:26

Model-Optimizer:统一管理优化器、学习率预热与EMA的训练技巧实践

训练一个模型&#xff0c;尤其是做微调的时候&#xff0c;最折磨人的往往不是网络结构怎么写&#xff0c;而是训练过程本身。Loss 曲线像个心电图&#xff0c;优化器换了又换&#xff0c;学习率调了又调&#xff0c;明明网络没问题&#xff0c;结果就是收敛得又慢又抖。我自己折…

作者头像 李华
网站建设 2026/9/30 4:16:15

Matlab实战:用高斯混合模型生成合成数据,实现数据增强与样本平衡

做数据分析的同学应该都遇到过这样的尴尬&#xff1a;手里有一份真实数据集&#xff0c;但因为保密要求或者采集成本限制&#xff0c;没法拿来放开手脚做实验&#xff1b;又或者某个类别样本太少&#xff0c;模型训练出来偏得离谱&#xff0c;一上线就现原形。今天聊的就是怎么…

作者头像 李华
网站建设 2026/9/30 4:15:01

OAuth2令牌存储全解:四种TokenStore方案对比与Spring Cloud实践

1. OAuth2令牌存储&#xff1a;为什么它是认证授权体系的命门1.1 先搞清楚令牌的完整生命周期做过微服务认证授权的同学都知道&#xff0c;OAuth2里最核心的东西不是那套授权流程&#xff0c;而是流程结束后拿到的那串token。很多新手第一次接触"令牌存储"这个概念时…

作者头像 李华
网站建设 2026/9/30 4:13:37

Android 13 WiFi ADB固定端口设置原理与实操指南

1. 项目概述&#xff1a;为什么Android 13的WiFi ADB必须设固定端口&#xff1f;Android 13系统对ADB调试机制做了几处关键调整&#xff0c;其中最影响日常开发和测试流程的&#xff0c;就是WiFi ADB默认端口的随机化行为。这不是Bug&#xff0c;而是Google在Android 13中引入的…

作者头像 李华
网站建设 2026/9/30 4:13:31

计及N-k安全约束的含光热电站优化调度模型Matlab实现

1. 从"怎么省成本"到"怎么扛风险"&#xff1a;N-k安全约束为什么必须引入光热调度做电力系统优化调度的朋友应该都有同感&#xff1a;传统经济调度模型跑起来很顺&#xff0c;约束也就是机组出力上下限、功率平衡、爬坡速率这几样&#xff0c;求解速度快&a…

作者头像 李华