“美食推荐商城”这类项目,在CSDN、GitHub和毕设选题里几乎到了“人手一个”的程度。但说句实话,大多数完成的版本只是给菜品做了个增删改查,所谓“推荐”就是按销量倒序排一下。真正能把“推荐”做出逻辑、把前后端分离写成工程化代码、把数据库设计到能扛住订单流转的项目,我这两年帮人看代码时确实见的不多。这篇就把我手头这套基于Java SpringBoot + Vue3 + MyBatis + MySQL的美食推荐商城的设计思路、核心代码落点、数据库建模、联调部署和答辩讲解方式完整过一遍。适合正在做毕设、准备找Java全栈工作、或者纯粹想学前后端分离项目写法的读者,目录结构可以直接照搬。
1. 先拆需求:美食推荐商城真正要做什么
很多人在动手前的问题不是“不会写”,而是“不知道写哪些功能”。一个叫“美食推荐商城”的项目,如果只做成餐饮版的商品列表,那就失去了“推荐”这个题目里的灵魂词语。我在设计时会把系统拆成三个角色视角:游客、注册用户、管理员。登录之后用户的画像、浏览历史、收藏记录才真正参与推荐计算,这样系统才有了“人”的味道。
1.1 推荐不是花架子,而是贯穿全流程的信号
我先明确一下“推荐”在这个系统里的具体落位:首页顶部是运营位横幅,下面接“猜你喜欢”板块;菜品详情页会推荐同分类的“更多美食”;用户下单完成后,在订单页提示“你可能还会喜欢”。这三个推荐点位不是孤立的UI模块,数据上分别对应三类算法策略:基于热度的全局榜单、基于用户偏好的个性化召回、基于行为相似度的协同过滤简洁版。这样设计的好处有两个:一是每个推荐位都能讲出计算逻辑,答辩时不会被问倒;二是实现难度可控,不需要上协同过滤框架,纯SQL加Java就能跑通。
热度榜单最简单,直接按销量和评分的加权排序。个性化召回要接用户表里的口味标签或收藏分类,用动态SQL查同类目菜品。协同过滤是这套系统里最能体现设计能力的部分:用一条联表SQL找“和你购买过相同菜品的人还在买什么”。这三个策略层层递进,数据量不大时全部走MySQL查询就能流畅执行;将来数据涨了,再把这套逻辑搬到Redis或离线计算里,也有清晰的演进路径。
1.2 前端用户端和管理端要当成两个工程来做
前后端分离的项目最容易犯的一个错误是:把管理员后台和用户商城塞进同一个Vue项目,靠路由权限硬切。我建议直接拆成两个前端工程目录,用户端侧重商品浏览、搜索、下单流程,移动端适配做得好一些;管理端侧重表格、表单、状态流转,使用Element Plus这类组件库效率会高很多。两个工程共用同一套后端API,但是通过不同的接口前缀和鉴权角色来区分访问范围。
后端这边我用SpringBoot搭的是经典三层结构:Controller负责收参和响应封装,Service层写业务规则和事务控制,Mapper层只面对SQL。订单创建、支付回调、库存扣减这类操作必须在Service层加事务注解,不能在Controller里直接操作Mapper,否则很容易出现只写了订单头没写订单明细这种脏数据问题。管理员端和用户端的登录状态用JWT来区分,网关层对需要角色的接口统一校验,这部分下文会展开。
1.3 功能清单:做到什么程度才算“完整可用”
如果照着毕设答辩或者“项目经验”的标准来卡,这套系统里至少得包含以下闭环能力:注册登录(含密码加密)、食材/菜品分类浏览、推荐列表、菜品详情、搜索筛选、购物车、提交订单、订单状态跟踪、评价与收藏、管理员对菜品和订单状态的维护。每一步都要形成数据闭环。比如购物车提交订单后,库存要扣减、销量要增加、订单要进入待付款状态;用户评价后,菜品评分要重新计算;收藏后,推荐列表要能刷出同分类内容。
这套闭环如果全部走通,项目在功能层面就是完整可用的,不会出现“演示时点完按钮没有任何反应”的尴尬场面。下面我会从技术点、数据库、部署三个方向,把每一环的落地方式讲透。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写代码才不烂尾:SpringBoot、MyBatis、Vue3的实战定位
确定完功能模块后,下一步是让每一项技术真正发挥作用,而不是只为了“凑技术栈”。我见过太多项目,SpringBoot只写了Controller,MyBatis只写了最简单的selectById,Vue3只用来展示静态数据。那样写完,面试官问两句就露馅了。这里我把三个技术栈的核心落点和编写心得分别说明,都是实际用过并且踩过坑的地方。
2.1 SpringBoot三件套:分层、事务、全局异常与统一返回体
后端如果不想烂尾,第一件事是定义一套统一的接口返回结构。我习惯用Result
然后是全局异常处理。用@RestControllerAdvice把业务异常、参数校验异常、兜底异常统一拦截,返回友好提示,避免把500错误堆栈直接暴露给前端。我遇到过不少项目,用户密码输错三次后页面直接显示一段Java异常,这在交付项目时属于严重减分项。
事务边界放Service层,这是这条链路里性价比最高的一行代码。以“创建订单”为例,逻辑上需要同时完成:生成订单主表记录、逐条写入订单明细、扣减菜品库存、累加菜品销量。任何一个环节失败,整个订单都应该撤销。在Service方法上打@Transactional(rollbackFor = Exception.class),就可以保证这些操作要么全成功要么全回滚。顺序很重要:先查库存判断是否充足,再执行更新与插入,后置校验容易产生并发超卖。
2.2 MyBatis别只写单表查询,动态SQL和一对多映射是亮点
MyBatis在这个项目里最能出彩的地方有两个:动态SQL和复杂结果映射。先讲动态SQL。商城系统的搜索条件经常是“多个条件可选组合”,比如前端传过来的是categoryId、keyword、minPrice、maxPrice、tag。如果不用动态标签,就得拼一堆if判断甚至干脆查全表再内存过滤,效率和代码可读性都很差。用MyBatis的<where>配合<if>,能自动处理拼接逻辑和多余的AND,代码看起来非常干净。我在菜品列表接口里大概是这么写的:
xml复制<select id="selectByFilter" resultMap="DishResultMap">
SELECT d.*, c.name AS category_name
FROM dish d
LEFT JOIN category c ON d.category_id = c.id
<where>
<if test="categoryId != null">
AND d.category_id = #{categoryId}
</if>
<if test="keyword != null and keyword != ''">
AND d.name LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="minPrice != null">
AND d.price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND d.price <= #{maxPrice}
</if>
<if test="tag != null and tag != ''">
AND FIND_IN_SET(#{tag}, d.tags)
</if>
</where>
ORDER BY d.sales DESC, d.rating DESC
</select>
这里顺便说一个细节:价格比较的>和<在XML里必须用>和<转义,否则XML解析直接报错;FIND_IN_SET是处理逗号分隔标签字段的便捷方法,字段规模大了之后再拆中间表,小项目用这个省事又够用。
另一个亮点是“一对多”映射。一道菜品会有多张轮播图,管理端编辑菜品时需要在同一次查询里取到图片列表,避免后续循环查询造成N+1问题。我先是给dish表保留一个主图字段,再建一张dish_image表存轮播图。查询时用MyBatis的collection把图片列表自动组装进去:
xml复制<resultMap id="DishResultMap" type="com.example.entity.Dish">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="price" column="price"/>
<result property="sales" column="sales"/>
<collection property="images" ofType="string">
<result column="image_url"/>
</collection>
</resultMap>
使用resultMap有一个连带要求:实体类字段和数据库列名要能对应上。最简单的方式是在application.yml里开启驼峰映射:mybatis.configuration.map-underscore-to-camel-case: true。这样数据库里的create_time自动映射到实体的createTime,省掉大量@Results注解。多对多映射比如“菜品配置多个食材标签”,做法和collection完全一致,只是中间表的JOIN条件多写一层。
MyBatis还有一个容易忽略的配置是Mapper接口扫描。启动类上记得加上@MapperScan("com.example.mapper"),否则每个DAO都要单独加@Mapper注解,新加Mapper类时特别容易漏。我在项目里统一用包扫描,一根依赖链下来,后端跑起来的出错率低很多。
2.3 Vue3组合式API配合Pinia,状态管理不再靠硬传参
前端部分我用的是Vite + Vue3 + Element Plus(管理端)的组合。Vue3推荐用组合式API组织逻辑,把接口请求、响应式数据、方法按业务域聚在一起,比如一个用户登录组件里就同时维护loginForm、loading状态和submit方法。和Vue2的Options API相比,这个模式在页面逻辑多的时候非常好维护。
跨组件共享数据是另一个高频场景,用户token、购物车数量、用户基本信息几乎每个页面都会用到。我直接引入Pinia作为状态管理,购物车做成一个store,Pinia里维护一个cartItems的ref数组,添加菜品时先判断是否已存在,存在则数量加一,否则push一条新记录。组件里只需要调用store的方法,不需要在路由或props之间来回传值,代码干净很多。
接口请求封装方面,我单独维护了一个request模块,基于axios实例统一配置baseURL和超时时间。请求拦截器里从Pinia取token并添加到请求头Authorization: Bearer <token>;响应拦截器里统一解包Result结构,遇到code为401时清空登录态并跳转登录页。这套拦截器一旦写好,后续每写一个接口,前端组件里只需要调用request.get('/api/dish/recommend')就能自动带上认证信息和错误处理,开发效率提升非常明显。
Vue3项目部署时还有一个容易踩的坑:打包后直接刷新页面出现404。原因是Vue Router默认使用history模式,刷新时Nginx找不到对应的物理路径。后文部署章节我会专门给出Nginx的try_files配置,这个配置能解决绝大多数刷新404问题。
3. 数据库表设计与推荐SQL:整个项目的亮点所在
如果你只想把这个项目当成普通商城做,数据库设计随便建几张表就够了;但如果你想在推荐逻辑上做出差异化,表结构必须为推荐服务。食堂里的梅菜扣肉做得再好看,没有合适的碗也端不上桌。表设计直接影响推荐SQL能不能顺畅写出来,所以这一个章节我重点讲清楚每一张表为什么而建,以及三条推荐SQL具体怎么写。
3.1 核心表结构与设计理由
我最终落地的表结构包含用户表、菜品分类表、菜品表、菜品图片表、订单表、订单明细表、评价表、收藏表这八张基础表,实际使用时还可以按需增加轮播图表。关键设计点用表格列出来:
| 表名 | 关键字段 | 设计意图 |
|---|---|---|
| user | id, username, password, avatar, taste_tag | taste_tag存用户口味偏好,推荐SQL直接使用 |
| category | id, name, sort | 分类层级简单,不做递归 |
| dish | id, category_id, name, price, description, main_image, tags, status, sales, rating | sales参与热度排序,rating由评价表聚合而来 |
| dish_image | id, dish_id, image_url | 放轮播图,避免主表字段过多 |
| orders | id, order_no, user_id, total_amount, status, create_time | 状态枚举:待付款、已付款、已发货、已完成、已取消 |
| order_item | id, order_id, dish_id, dish_name, price, quantity | 冗余菜品名称和价格快照,订单历史永不变形 |
| comment | id, user_id, dish_id, content, rating | 评分数据源 |
| favorite | id, user_id, dish_id, create_time | 收藏数据,参与推荐召回 |
有三处设计我认为很关键。第一,订单明细表冗余了dish_name和price,而不是在查询时去JOIN菜品表。原因很简单:商家修改菜品价格或名称后,历史订单不该跟着变,快照能保证报表数据的真实。第二,金额字段一律用decimal(10,2),绝不用double或float存储钱,这是金融级常识,浮点误差在订单合计时很致命。第三,菜品表加了一个sales字段用于订单完成后的累加统计,而不是每次推荐都去订单明细表里count,虽然稍显冗余,但查询速度完全不用担忧。
3.2 推荐逻辑第一层:基于热度的冷启动榜单
新用户没有任何行为数据,推荐系统要做冷启动。我这里的做法是维护一个“今日热卖”接口,SQL逻辑按销量权重为主、评分为辅计算热度分。热度分算法很简单:热度 = 销量 * 0.7 + 平均评分 * 100 * 0.3。评分模数100是为了让销量和评分的数值量级接近,比如卖出了200份的评分4.5分菜品,热度分是140加135,排序更均衡。SQL写进Mapper:
sql复制SELECT id, name, price, main_image, tags, sales, rating,
(sales * 0.7 + rating * 100 * 0.3) AS heat_score
FROM dish
WHERE status = 1
ORDER BY heat_score DESC
LIMIT #{limit}
这个方案在数据量不大时完全够用。唯一要注意的是这里没有加LIMIT过大导致的性能问题,推荐位默认显示6到8个就好。管理员面板里还可以基于这张表做一个简单的运营数据看板,展示每个分类下的销量Top3,后端一个分组查询就能出结果。
3.3 推荐逻辑第二层:用户画像召回
用户登录后,推荐系统要进入个性化阶段。用户表的taste_tag字段在注册时让用户选择,字段存的是逗号分隔的标签,比如“川菜,烧烤,甜点”。那么针对注册用户的“猜你喜欢”SQL,可以先用FIND_IN_SET匹配口味标签,再关联菜品表找有这些标签的菜品,最后过滤掉用户已经买过的菜品。
更实用的方式是基于用户收藏和下单记录自动推断分类偏好。先查用户收藏最多的分类id,再从这个分类中挑销量高的菜品。我用一条子查询搞定两类召回:
sql复制SELECT d.id, d.name, d.main_image, d.price
FROM dish d
WHERE d.category_id IN (
SELECT c.id FROM category c
WHERE c.name IN (
SELECT tag FROM user WHERE id = #{userId} -- 简化示意,实际可用收藏统计
)
)
AND d.status = 1
AND d.id NOT IN (
SELECT oi.dish_id FROM order_item oi
JOIN orders o ON oi.order_id = o.id
WHERE o.user_id = #{userId}
)
ORDER BY d.sales DESC
LIMIT 8
实际项目里我并没有真用user表里的字符串去匹配分类名,而是用了favorite表的收藏分类统计,逻辑为:取用户收藏次数最多的分类id,再推荐该分类下的热销菜品。这样推荐的依据来自用户操作行为,说服力比注册选择强得多。过滤子查询里的“已购买”是必须的,如果真的把用户买过的菜再推荐一遍,体验感就全毁了。
3.4 推荐逻辑第三层:简单协同过滤实现“和你口味相似的人都在买”
协同过滤是这个项目里最能体现算法能力的地方,虽然只是简化版,但从面试角度讲完全够用。整体思路分三步:找出与当前用户有共同购买行为的用户;统计这些用户还购买了哪些当前用户没买过的菜品;按共同购买次数和菜品热度排序推荐。
这里我用一条JOIN SQL来表达,比较容易理解:
sql复制SELECT d.*, COUNT(DISTINCT other_u.id) AS common_cnt
FROM order_item oi1
JOIN orders o1 ON oi1.order_id = o1.id AND o1.user_id = #{currentUserId}
JOIN order_item oi2 ON oi2.dish_id = oi1.dish_id AND oi2.order_id != oi1.order_id
JOIN orders o2 ON oi2.order_id = o2.id AND o2.user_id != #{currentUserId}
JOIN user other_u ON o2.user_id = other_u.id
JOIN dish d ON oi2.dish_id = d.id
WHERE oi2.dish_id NOT IN (SELECT dish_id FROM order_item oi WHERE oi.order_id IN (SELECT id FROM orders WHERE user_id = #{currentUserId}))
GROUP BY d.id, d.name, d.main_image, d.price, d.sales, d.rating
ORDER BY common_cnt DESC, d.sales DESC
LIMIT 8
这条SQL的运行逻辑是:拿到当前用户买过的所有菜品;找到所有也买过这些菜品的其他用户;统计这些其他用户购买过哪些当前用户没买的菜品;最终按共同购买人数降序返回。共同购买人数越多,说明这两个用户的品味越接近。这条SQL在百万级订单数据下可能会慢,但在毕设和中小型商城里性能绰绰有余。我在Mapper中会为d.id加索引,并且orders表的user_id也要建索引,JOIN性能才有保障。这条SQL写出来,整个项目的技术含金量立刻上了一个档次。
4. 联调、部署与高频报错排查:从能跑变成能上线
功能写完了,项目停在本地“能跑”阶段其实只完成了七成。前后端联调、部署上云、上线后的报错排查,才是区分“课程作业”和“可交付项目”的分水岭。这一章我把联调期最常踩的坑、部署时的Nginx配置以及我积累的一个高频问题速查表全部整理出来。
4.1 联调期最容易踩的四个坑
第一个是跨域问题。前端开发服务器在5173端口,后端接口在8080端口,直接访问会被浏览器拦截。不要在后端代码里做一个允许所有来源的CorsFilter,这在开发时方便,部署后容易埋雷。我推荐开发环境用Vite的proxy配置把/api代理到8080,生产环境用Nginx反向代理,本质都是让浏览器只访问同源地址,跨域问题自然消失。
第二个是认证信息传递不一致。JWT令牌要统一放在请求头Authorization里,后端拦截器统一从这里取。最忌讳的是前端有的接口用params传token、有的接口用header传token,后端拦截器没法统一处理,一调一个401。
第三个是时间格式问题。后端返回的LocalDateTime默认是ISO格式,前端显示成本地时间经常差8小时。统一在application.yml里配置Jackson:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
数据库连接串也要加上serverTimezone=Asia/Shanghai。这两处配置不配套,订单创建时间很可能显示成UTC时区的时间,排查起来非常隐性。
第四个是文件上传大小限制。菜品图片轮播图如果用Base64字符串塞给后端,很快会把Tomcat的默认限制打满。前端要改用multipart文件上传,同时在后端配置spring.servlet.multipart.max-file-size=10MB和max-request-size=50MB。本地联调时Base64能用,一上线就报“Current request is not a multipart request”或“SizeLimitExceededException”,排查半天最后发现是请求体超限。
4.2 从本地到云服务器的部署方案
后端打包用mvn clean package,成功后生成jar包。启动命令我一般写一个带环境参数的脚本:
bash复制nohup java -jar food-recommend-1.0.0.jar \
--spring.profiles.active=prod \
--server.port=8080 \
> logs/app.log 2>&1 &
这里是老生常谈的nohup加日志重定向,好处是SSH断开后进程依然运行。生产环境MySQL连接串要显式指定参数:jdbc:mysql://localhost:3306/food_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。其中useSSL=false能省去一大堆证书告警,本地连MySQL时如果报SSL握手错误,大概率就是因为这行。
前端打包执行npm run build,产物在dist目录。把dist目录整个上传到服务器,用Nginx指向这个目录。我给出最核心的配置片段:
nginx复制server {
listen 80;
server_name your-domain.com;
root /var/www/food-front;
index index.html;
# 解决history路由刷新404
location / {
try_files $uri $uri/ /index.html;
}
# 后端API反向代理
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这里最需要注意的是proxy_pass的URI格式。如果写成proxy_pass http://127.0.0.1:8080;不带/api/,那么请求/api/dish/list会被原样转发为/api/dish/list,后端Controller要是以/api作为context-path前缀才能匹配;带/api/后转发路径是/dish/list,后端接口就可以不写/api前缀。两种方式都能用,但前后端必须约定一致,否则接口404。我个人习惯是在Nginx层去掉/api前缀,后端Controller统一用/dish/list这种短路径。
4.3 高频报错与排查技巧速查表
我把项目开发中高频出现的报错和排查方向整理成一张速查表,方便对号入座:
| 现象 | 常见原因 | 排查与解法 |
|---|---|---|
| 前端接口全部404 | Nginx代理地址写错或后端没启动 | 先curl http://127.0.0.1:8080/dish/list 验证后端 |
| 刷新页面404 | Vue Router history模式没有try_files | 在Nginx配置加try_files $uri $uri/ /index.html; |
| 查询结果中文乱码 | 数据库连接没指定UTF-8 | 连接串加characterEncoding=utf8,检查库表和连接三处字符集 |
| 请求返回401 | token过期或拦截器没放行登录接口 | 在登录接口上加白名单,前端检查请求头是否携带token |
| MyBatis报“Invalid bound statement” | Mapper接口和XML没绑定 | 检查XML的namespace是否与接口全限定名一致 |
| 数据插入后缺少create_time | 数据库字段有默认值但实体没同步 | 插入语句不要带create_time,让数据库默认值生效 |
| 前端跨域报错 | 开发环境没配代理,直接访问8080 | 在vite.config.js中配置server.proxy |
还有一个我特别想提醒的排查思路,就是遇到报错先看后端日志。很多人一遇到页面报错就闷头改前端代码,结果后端日志里明晃晃写着“SQL语法错误”或者“空指针”。我在部署脚本里把日志输出到logs/app.log,排查问题第一条命令永远是tail -100 logs/app.log。后端日志干净了,前端的问题往往就只剩网络交互和数据结构。
5. 答辩与面试视角:怎么把这套系统讲出竞争力
代码写完只是第一步。对于毕设答辩和求职面试来说,怎么把项目讲得有条理、有深度,和代码一样重要。很多人的项目代码并不差,但一开口就是“我用了SpringBoot+Vue3,做了登录注册、商品管理、订单管理”,面试官听完完全无感。这里我给出一套我自己验证过好用的讲述思路和延伸问题准备清单。
5.1 五句话讲完项目全貌,别按模块背流水账
给项目搭讲解框架时,我建议按这个顺序组织:先说业务场景,再说架构方案,最后说难点亮点。比如这样开场:“我做的是一个面向餐饮场景的推荐商城,核心矛盾是帮助用户在众多菜品中快速找到符合口味的选择。整体采用前后端分离,后端以SpringBoot为主体提供RESTful接口,前端Vue3负责用户端和管理端的交互,推荐模块由热度榜、用户画像召回和简单协同过滤三层策略组成。我重点解决的问题是让推荐结果能结合用户行为动态变化,以及保证下单过程中库存和订单数据的一致性。”
这段介绍里包含了业务、架构、算法、数据一致性四个关键点,每个点都值得面试官追问。这时候千万不要自己把细节全倒出来,而是留出“钩子”,等面试官问你“这个协同过滤怎么实现的”“订单和库存一致性怎么保证”,你就有充分空间展示思考深度。我在陪读者模拟面试时反复强调:项目讲解的本质是引导提问,而不是背诵简历。
5.2 面试官必问的延伸问题与应答方向
根据这套系统的技术栈,我整理了六个出现频率极高的问题,并提供思考方向供参考。
第一个,SpringBoot的自动配置原理。不要只背“约定优于配置”,要能说出@SpringBootApplication由@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个注解组成,自动配置核心是spring.factories文件和条件注解@ConditionalOnClass。结合自己的项目说一句“所以我在引入MyBatis依赖后,只要配置好数据源就能直接用SqlSessionFactory”,就比背概念强很多。
第二个,MyBatis的#{}和${}区别。核心回答是#{}编译为占位符,能防止SQL注入,而${}是字符串直接拼接,主要用于表名、排序字段这种动态位置。项目里凡是参数值都该用#{},如果非要用${}必须做白名单校验,我真实遇到过用${}拼接排序字段导致数据库被拖库的案例,面试讲到这个会加分。
第三个,事务失效有哪些场景。结合订单功能,至少要能说出:方法被非public修饰、方法内部自调用绕过代理、@Transactional放在Controller层、rollbackFor没指定导致抛出的运行时异常没有回滚、异常被try/catch吞掉。我在代码审查时经常看到有人把catch里打日志后又throw出来,这种情况事务依然能回滚;但如果是catch后return,事务就不回滚了,这是最容易犯且最隐蔽的一个。
第四个,前后端分离下的会话管理方案。JWT和Session二选一,以及为什么选JWT。我的回答是:商城系统将来可能同时支撑Web、H5、小程序多端,JWT无状态特性天然适合,服务端不用维护Session,水平扩展时可以随意加节点;代价是token注销不方便,所以我在token里加了过期时间并配合Redis做黑名单。这个延伸说明比单纯说“JWT好”有说服力得多。
第五个,如果数据量增大,你的推荐SQL怎么办。这个问题主要考察可扩展思维。我的改进方向是:冷热数据分离,把销量TOP500的菜品和热门推荐结果缓存到Redis;把离线计算用户偏好和协同过滤结果定时生成到一张推荐结果表,在线接口只做查询;引入Elasticsearch替换模糊搜索;推荐服务独立成微服务,通过RPC给商城API供数。不要为了显得高级而引入自己都说不清的中间件,能讲清楚一个Redis缓存策略就已经很扎实。
第六个,用户密码存在数据库里怎么存。项目里我用的是BCrypt加密,不使用MD5加盐这种弱方案。BCrypt是自适应哈希算法,同一密码每次生成的密文都不同,而且计算成本可调。面试官追问“为什么不用MD5”时,可以回答:“MD5是摘要算法而非加密算法,速度快所以容易被暴力破解,即使加盐也扛不住GPU并行计算;BCrypt内部包含随机盐且计算代价故意设计为耗时,能显著提高破解成本。”
5.3 答辩现场别犯的三个低级错误
答辩和面试其实很吃细节。我总结了三个平时不容易注意到但会影响评价的细节。第一,演示前务必先清理浏览器缓存和登录态,不要一上来就跳转到上次开发人员留下的报错页面,这个印象分一掉就很难拉回来。第二,准备好一份常见接口的Postman或Apifox导出,万一前端页面抽风,可以直接用接口工具演示业务逻辑正常,说明问题不在后端。第三,不要反复强调代码是“自己一个人写的”,有水平的回答方式是把项目的模块结构讲清楚,具体到自己负责了哪些模块,遇到的最难问题是什么,怎么解决的,对方自然会感受到工作量。
最后说点真心话
我带着这套系统的设计思路和代码经验陪跑过不少读者,也帮人排查过无数个“本地明明好,一部署就崩”的诡异问题。如果你是自己一个人独立啃这个项目,我的建议是:不要一上来就疯狂写代码,把数据库表和推荐SQL想明白再动手,这张设计图至少能帮你节省一半返工时间。从前端到后端,从推荐逻辑到部署上线,每一个环节你亲手走一遍,比看十篇别人的项目总结都有用。代码打包起来,数据库表结构建好,再配合我第三节里的推荐SQL,你做出功能完整、逻辑清晰、能讲出技术亮点的美食推荐商城,只是时间问题。
