做了两三个月的美食探店平台,期间踩了无数坑,也积累了不少实战经验。今天把整个项目从设计到落地的完整过程整理出来,涵盖技术选型、数据库设计、前后端对接、部署上线这几个关键环节,希望能给正在做类似 web 系统开发的朋友一些参考。
这个项目最初的定位很简单,就是想做一个让用户发现周边美食、查看真实评价、记录探店心得的平台。市面上的点评产品功能已经非常庞杂,反而让“认真记录一次探店体验”这件事变得很重。所以我在设计这个平台时,刻意做减法,只保留三个核心环节:找店、看评价、写笔记。整个系统基于 web 架构实现,前端负责交互展示,后端提供数据接口,数据库存储业务数据,这也是目前中小企业级 web 项目比较主流的分层方式。
1. 整体思路与架构设计
1.1 功能定位与用户场景
动手写第一行代码之前,我花了不少时间梳理用户场景。对着需求文档反复问自己:用户到底是怎么用这个平台的?我把使用路径分成了三类核心场景,分别对应平台的三类典型用户。
第一类是普通食客,他们的核心动作是“找店”。打开首页,系统根据地理位置推荐附近评分高的店铺,或者按照菜系、人均价格筛选目标范围的商家。这类用户对响应速度很敏感,首页从发出请求到渲染出店铺列表,超过三秒就会流失相当一部分访问量。
第二类是探店达人,他们的核心动作是“写”。记录一次探店的真实体验,上传环境照片,给菜品逐项打分,输入一段真实的感受文字。这部分用户贡献了平台最核心的内容资产,平台上所有的店铺评价、探店笔记都来自他们。设计时需要特别注意编辑体验的流畅度,以及图片上传后的加载速度优化。
第三类是商家侧的用户,店主登录后台管理自己的店铺信息,回复用户评价,发布优惠活动。实际开发时考虑过给商家做一个独立管理端,后来评估下来,一期版本直接在现有平台里加一个商家角色权限就能覆盖需求,省去了单独部署一套后台系统的成本。
明确了用户场景之后,核心功能模块就很清晰了,包括用户认证、店铺信息管理、评价和探店笔记、基于地理位置的推荐、关键词搜索。每个模块我都在后续架构设计里对应到了具体的表结构和接口上,功能和数据一一对应,开发过程中基本上没有出现过“功能写完了却发现没地方存数据”的情况。
1.2 技术选型的思考过程
技术选型这件事,我做了几个维度的对比。前端框架在 Vue 和 React 之间犹豫过一阵,后来考虑到团队小伙伴更熟悉 Vue 的生态,加上这个项目实际用到的是大量表单交互和列表展示,Vue 的双向数据绑定能让这类开发效率明显提升,最终选了 Vue 3 + Element Plus 的组合。前端工程化基于 Vite 构建,开发环境热更新速度确实比之前的 Webpack 方案快了一个量级。
后端原本有两个候选方案,一个是 Java 的 Spring Boot,一个是 Node.js 的 Express。两个方案我都有实际项目经验,最终选择 Spring Boot 的理由很实在:第一,社区生态成熟,遇到问题几乎都能搜到现成的解决方案;第二,自带的安全机制比较完善,Spring Security 在处理用户认证和权限控制时比自己在 Node 里拼装中间件要稳妥很多;第三,项目后续如果要做高并发扩展,Spring Boot 和微服务生态的衔接更加顺畅。这个决策在开发后期印证了是对的,出问题的几个地方都靠成熟的框架机制快速定位了。
数据库选了 MySQL 8.0 加 Redis 的组合。MySQL 是标准的关系型数据库,店铺、用户、评价这些业务实体之间有天然的关联关系,用关系型模型表达最直接。Redis 用来做热点数据的缓存,比如首页热门店铺列表、用户登录会话等,这部分数据读多写少,放缓存里能显著降低数据库压力。
1.3 系统整体架构分层
整个系统按标准的三层架构来组织。表现层由 Vue 构建的单页应用负责,所有页面通过 RESTful API 与后端通信。应用层是 Spring Boot 提供的接口服务,内部又细分成控制器层、业务逻辑层、数据访问层。控制器层只负责参数校验和结果封装,业务逻辑层处理核心规则,数据访问层基于 MyBatis-Plus 操作数据库。
数据层包含 MySQL 主数据库和 Redis 缓存,MySQL 存所有持久化数据,Redis 做缓存和分布式会话管理。静态资源部分,图片文件单独放在 Nginx 代理的目录下,和应用服务器分离,避免图片访问的 IO 压力把接口拖垮。
系统架构确定之后,我画了一张详细的请求链路图,从浏览器发起请求到最终返回渲染,每个环节都明确了职责。前端发起请求后,Nginx 做反向代理,把动态请求转发到 Spring Boot 应用,静态资源直接由 Nginx 返回,应用层处理完业务后返回 JSON,前端渲染更新页面。整个链路在实际运行中非常清晰,排查问题时能快速定位是前端、后端还是网络环节的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心数据模型
2.1 核心表结构设计
数据库设计是整个项目的地基,表结构如果设计得不合理,后续写业务逻辑处处受制。我按业务边界把数据拆成了四个核心区域:用户域、店铺域、评价域、互动域。
用户域的表很简单,user 表存储账号信息和基础资料。账号字段用了手机号做唯一索引,密码字段存储的是 BCrypt 加密后的密文,绝不能明文入库。用户资料放在同一张表里,包括昵称、头像 URL、个人简介,加了一个 status 字段控制账号状态,正常为 1,封禁为 0。
店铺域的核心表是 shop 和 shop_category。shop 表字段非常多,除了店铺名称、地址、联系电话这些基础信息,还有经纬度坐标、菜系分类、人均消费、评分、营业时间。经纬度字段单独拿出来说,我用的是 DECIMAL(10, 7) 类型存储,因为后面要基于地理坐标做附近推荐查询,精度和查询效率都要兼顾。shop_category 是一个简单的分类表,存储一级和二级菜系分类,通过 parent_id 字段支持层级结构。
评价域的 design 花费的精力最多。review 表存储用户对店铺的评价,有效评分字段包含综合评分、口味评分、环境评分、服务评分四个维度,这样用户打分的时候可以分项评价,列表展示时也能按不同维度排序。review_image 表单独存评价关联的图片,一张评价对应多张图,所以单独抽出来做一对多。
sql复制CREATE TABLE `shop` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`shop_name` varchar(100) NOT NULL COMMENT '店铺名称',
`address` varchar(255) NOT NULL COMMENT '地址',
`longitude` decimal(10,7) NOT NULL COMMENT '经度',
`latitude` decimal(10,7) NOT NULL COMMENT '纬度',
`category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
`avg_price` int(11) DEFAULT '0' COMMENT '人均消费',
`rating` decimal(3,2) DEFAULT '0.00' COMMENT '综合评分',
`status` tinyint(1) DEFAULT '1' COMMENT '状态 1营业 0关闭',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_location` (`longitude`, `latitude`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建这个表的时候有一个细节值得说一下,就是经纬度字段要不要建联合索引。早期测试数据量小的时候不建索引也没问题,等数据量到了几十万条的时候,基于位置的查询就会慢得明显。后来我同时建了联合索引和空间索引做对比测试,发现这个量级下联合索引的效果更好,查询不需要做额外的几何计算,直接走 B+ 树的区间扫描就行。
2.2 Redis 缓存设计
Redis 在这个项目里主要干两件事:缓存热点数据和存储用户登录态。热点数据包括首页推荐店铺列表、热门榜单、店铺详情页的基础信息。这类数据有典型的读多写少特征,用户访问量大,内容更新频率低,缓存命中率很高。
我设计缓存 key 的时候遵循了一套固定的命名规范,业务模块用冒号分隔,例如 shop:hot:list 表示热门店铺列表,review:count:shopId 表示某个店铺的评价数量。设置过期时间时也有讲究,首页推荐列表设置五分钟过期,店铺详情页的基础信息设置三十分钟过期。遇到用户新增评价导致店铺评分变化的情况,不需要手动去更新缓存,等缓存有效期过了之后自动回源数据库重新加载即可,这个策略叫 Cache Aside Pattern,实现简单、逻辑清晰,非常适合这个量级的应用。
用户登录态用 Redis 存储 Token 对应关系,登录成功之后生成一个 UUID 作为 Token,以 Token 为 key、用户 ID 为 value 存入 Redis,设置二十四小时过期。相比传统的 Session 方案,这样做的好处是天然支持水平扩展,即使后面把应用部署到多台服务器,只需要共享同一个 Redis,用户的登录状态在每台机器上都是可用的。
实际开发中踩过缓存穿透的坑。攻击者或者异常请求不断查询一个不存在的店铺 ID,每次都穿透到数据库,造成不必要的压力。我采用的解决方案是缓存空值,当查询一个不存在的店铺时,把空结果也缓存起来,过期时间设短一些,比如三分钟,能有效阻止穿透问题。如果是更复杂的场景,可以考虑用布隆过滤器做前置拦截,但对这个项目来说,缓存空值已经足够解决实际遇到的问题了。
3. 核心功能模块的实现细节
3.1 用户注册与登录体系
用户认证模块是整个系统安全的入口,这块如果做薄了,后面所有依赖用户身份的功能都会有问题。注册流程我用的是手机号加验证码的方式,验证码通过短信服务商接口发送,存储时只存验证码的 Hash 值而不是明文,校验通过后立即删除,防止验证码被重复使用。
密码设计遵循了当前业内比较通行的做法,用 BCrypt 算法做哈希处理。BCrypt 的一个特点是每次生成的盐值随机,所以同一个密码两次加密后的结果完全不同,这样即使数据库泄露,攻击者也无法通过彩虹表反推原始密码。 Spring Security 的内置 BCryptPasswordEncoder 可以直接使用,不需要自己实现加密逻辑。
登录成功后的 Token 机制做了一个小改进。用户登录后生成的 Token 除了存 Redis,还会在响应时把 Token 同时返回给前端。前端存储在 localStorage 里,后续每个请求都通过拦截器把 Token 放入 Authorization 请求头。后端在 Filter 层校验 Token 有效性,校验通过后把用户信息存入 ThreadLocal,这样在业务代码里通过一个工具方法就能拿到当前登录用户。
code复制权限控制这块,最简单的做法就是区分角色。普通用户和商家是两种不同的角色,商家才能进入后台管理自己的店铺。Spring Security 的注解权限控制在方法级别来做校验,比如 `@PreAuthorize("hasRole('MERCHANT')")` 标注在商家接口上,普通用户访问时会直接被拒绝并返回 403。
3.2 店铺信息的增删改查
店铺信息的 CRUD 是平台的基础能力,商家端和用户端都依赖它。管理端需要支持店铺的创建、编辑、上下架,用户端则主要读取店铺列表和详情页。
商家提交店铺信息时的表单校验做了好几层。前端做了一层基础校验,比如店铺名称不能为空、地址长度限制、人均价格必须为正整数。后端的校验更加严格,用 JSR 303 注解做 Bean Validation,@NotBlank、@NotNull、@DecimalMin 这些注解直接标注在 DTO 字段上,框架在进入业务逻辑之前就完成校验。我特别处理了经纬度格式,使用 @Pattern 正则校验纬度范围在 -90 到 90、经度范围在 -180 到 180,不合法就直接返回参数错误。
列表查询支持多条件筛选,包括菜系分类、人均价格区间、评分排序、距离远近。实现时用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼接查询条件,根据参数是否为空来决定是否添加对应条件。这里需要留意模糊查询的性能问题,店铺名称的模糊搜索在数据量大时会失去索引,我的方案是限制模糊查询必须至少输入两个字符才能执行,避免用户输入单个字符就把全表扫一遍。
距离排序通过 MySQL 的数学函数计算。假设用户当前位置经纬度是固定的两个变量,查询时按照距离公式计算结果排序,用 HAVING 子句过滤出三公里范围内的店铺。这个写法在数据量小于十万条时性能表现良好,超过这个量级就需要考虑把地理查询下沉到 Elasticsearch 或者 MongoDB 的 Geo Query 了,这对于初期项目来说完全够用。
3.3 评价系统与评分计算
评价系统是美食探店平台最核心的功能,用户愿意看评价、写评价,平台才有持续的 UGC 内容产出。评价流程设计上做了几个关键决策。
用户在店铺详情页可以发表评价,评价内容包括四维评分和文字内容。评分控件用星级组件来实现,打分时是半星精度,后端接收到的是 0.5 到 5.0 之间的数值。数据库里评分字段定义为 DECIMAL(2,1),可以精确存储一位小数。提交评价时还允许上传最多九张图片,图片先通过前端直传到 OSS 或者 Nginx 静态目录,拿到 URL 后再随评价内容一起提交。这样的好处是评价提交接口本身不处理文件上传,响应速度快,也便于后期扩展图片处理服务。
评分计算是这里面的关键逻辑。每个店铺的综合评分不是简单求所有评价的平均值,我做了加权处理:近三个月的评价权重更高,老评价的权重逐月递减。这样设计是为了让店铺评分能更快反映最近的经营状况,避免一家店以前很好但最近质量下滑时评分还保持虚高。实际实现时,我用一个定时任务每天晚上离线重算各店铺的综合评分,并更新到 shop 表的 rating 字段。同时,评价提交接口里也会实时更新一次评分,保证用户立即看到最新结果。
UGC 内容治理在评价系统里不能忽视。我做了两个基础的文本处理:敏感词过滤和垃圾评价拦截。敏感词用前缀树的算法做匹配,命中后直接拒绝发布,并提示用户修改。垃圾评价拦截通过规则判断,比如评价文字长度小于五个字、全是标点符号、短时间内连续提交多条评价,这些情况都会被系统拦截。后来加上了简单的关键词黑名单,效果提升了大概百分之三十。
3.4 探店笔记与用户关系链
探店笔记是区别于普通评价的内容形态,更接近一篇图文并茂的短文。用户可以在笔记中记录完整的探店过程,配图、排版、分享自己的真实感受。这部分设计比较接近轻量级的社区发布功能。
笔记表结构设计时,我单独创建了 note 表,字段包括标题、正文内容、封面图、关联店铺 ID。正文内容虽然支持纯文本,但为了兼容长文章和未来可能的富文本编辑器扩展,字段类型用了 LONGTEXT。笔记和标签是多对多关系,单独建了 note_tag 关联表,方便后续按标签聚合内容。
用户关系链做了最简单的关注功能,分为 user_follow 表记录关注关系。关注功能带动了首页信息流的实现:用户登录后可以看到自己关注的探店达人的最新笔记,按照时间倒序排列。这个功能架构上用的是推模式,用户发布笔记时直接把笔记 ID 写入每个粉丝的收件箱列表,读取时直接按序取,不必实时聚合关注列表,数据一致性稍微牺牲了一些但读取性能很理想。
3.5 搜索与推荐模块
平台的搜索功能经历了两个阶段的演化。第一阶段用 MySQL 的 LIKE 模糊查询,店铺名称、菜系标签都通过字符串匹配实现。数据量一万条以内时体验尚可,但随着内容增长,查询响应时间明显变差,而且 LIKE 查询不支持中文分词,搜“川菜”时无法匹配“四川风味”这样的描述。
第二阶段引入了 Elasticsearch 做搜索,把店铺名称、地址、菜系、标签这些字段建立索引,配合 IK 分词器实现中文分词。店铺数据通过定时任务同步到 Elasticsearch,用户搜索时走搜索接口,返回店铺 ID 列表后再回 MySQL 查完整数据。第一次接入 Elasticsearch 时踩了版本兼容的坑,客户端版本必须匹配服务端版本号,不然连接直接失败,后来仔细核对版本之后才跑通。
推荐模块做的是基于规则的召回加排序。用户进入首页,系统根据用户历史交互行为生成候选店铺集合:用户浏览过的店铺同菜系的店铺、用户关注达人评价过的店铺、以及当前城市评分排名靠前的店铺。排序上综合了三个维度的得分:内容相似度、评分相关度、热度衰减因子。规则化的推荐系统效果虽然不如机器学习模型那么精细,但对于冷启动的初创平台来说已经十分匹配——没有大量用户行为数据的时候,深度学习模型也发挥不出作用。
4. 前后端联调与关键实现
4.1 RESTful API 规范与联调流程
接口规范直接决定了前后端协作的效率,这部分我坚持在开工前和前端明确了约定。接口设计遵循 RESTful 风格,URL 用名词复数表示资源,通过 HTTP 方法区分操作。例如 GET /api/shops 表示获取店铺列表,POST /api/shops 表示创建店铺,GET /api/shops/{id} 表示获取店铺详情。
统一的响应格式是联调顺利的另一个关键。所有接口返回 JSON 都遵循固定结构:包含 code 状态码、message 提示信息和 data 数据三部分。状态码不是单纯复制 HTTP 状态码,而是业务层自定义的,比如 200 操作成功、400 参数错误、401 未登录、403 无权限、500 服务器异常。前端拿到响应后先判断 code,再决定是渲染数据还是弹出错误提示。
联调过程中容易遇到的坑大部分是数据类型不一致。后端的 Long 类型主键在前端处理时会产生精度丢失,因为 JavaScript 的 Number 类型安全整数范围有限。解决方案是在后端统一配置序列化器,把 Long 类型转换为 String 输出给前端,前端拿到字符串再进行操作。发现问题时已经在测试环境跑了大半天,上线前能修掉都算幸运。
4.2 前端核心页面实现要点
前端实现了五个核心页面:首页、店铺列表页、店铺详情页、探店笔记发布页、个人中心页。每个页面的开发都有一些值得记录的细节。
首页的设计目标是让用户在三秒内理解平台价值。顶部是搜索栏,支持直接搜索店铺,中间区域是地理位置的快捷筛选入口,以下是基于用户位置推荐的卡片式店铺列表。推荐接口需要用户授权位置信息,前端通过浏览器的 Geolocation API 获取经纬度,用户拒绝授权时降级为默认城市的推荐列表。
店铺详情页的信息架构依次是店铺头图、基础信息卡片、评分模块、用户评价列表、探店笔记列表。页面采用了骨架屏方案,数据加载完成前展示灰色占位区块,避免白屏造成的焦虑感。评价列表使用虚拟滚动优化性能,当评价数量较多时,只渲染可视区域内的 DOM 元素,滚动过程中动态加载。实测两千条评价数据时页面滚动依然流畅,普通全量渲染的方式在超过五百条时掉帧就明显了。
笔记发布页看似简单,实际实现也有一些细节需要处理。图片预览组件要支持压缩上传,大尺寸的图片在前端先行压缩到宽度不超过一千二百像素,再上传服务器,既减少了带宽消耗又提升了上传速度。正文编辑器用的是开源的文本编辑器组件,存储时保存为纯文本格式,渲染时再做段落解析,灵活性比直接存 HTML 要强一些。
4.3 性能优化实践
性能优化贯穿了整个开发周期。从一开始就留意各个方面的问题,不等所有功能完成才开始测试,而是每个模块开发完成后立即进行针对性优化。
首屏加载速度优化是前端工作的重点。通过 Vite 的代码分割功能,将路由页面拆分成独立的 chunk,按需加载。第三方库通过 CDN 引入,减少了打包体积。首次访问页面时,只有公共部分和当前路由对应的代码会被加载,其他页面的代码等到用户真正导航过去时才下载。优化后首屏资源从约 1.8MB 减少到约 700KB,在正常网络环境下的加载时间从平均 3.2 秒缩短到 1.4 秒。
数据库查询优化主要集中在索引调整和查询重构。刚开始店铺列表接口慢,通过慢查询日志定位后,发现 ORDER BY rating DESC 导致文件排序,给 rating 字段加上索引之后解决。另外一个常见问题是 N+1 查询,遍历店铺列表时逐条查询店铺评价数量,后来改成用一条 GROUP BY 查询一次性拿到所有店铺的评价数量,循环中直接读取。
图片加载的优化采用了懒加载和响应式图片方案。店铺列表中的封面图使用 loading="lazy" 属性,滚动到可视区域才触发加载。不同屏幕尺寸下的图片通过 srcset 属性提供不同分辨率的版本,手机端不需要加载和电脑端一样的大图。这个优化的收益在移动端特别明显,节省了相当可观的流量。
5. 部署上线与常见问题排查
5.1 环境部署与上线流程
部署环境用一台 Linux 服务器搭建,应用通过 Nginx 反向代理对外提供服务。Nginx 负责请求转发和静态资源服务,Spring Boot 应用以 Jar 包形式运行在后端端口。因为开发阶段使用的是 dev 环境的配置,部署时通过 spring.profiles.active 参数切换为 prod 配置,数据库连接、Redis 地址、日志级别等配置都在配置文件里区分。
部署流程做了一套简单的自动化脚本,脚本通过 Git 拉取最新代码,执行打包,然后停止旧进程、备份旧版本、启动新版本,最后检查健康检查接口确认服务正常。这套流程虽然不算 CI/CD 的完全自动化,但已经把手工部署最容易出错的部分自动化了。上线初期,我每天都需要部署好几次,每次手动操作不仅耗时,还容易出错,用了脚本之后稳定了很多。
域名和 HTTPS 是上线前我特别关注的。通过 Let‘s Encrypt 申请了免费证书,在 Nginx 配置中强制 HTTPS 跳转,保证所有接口的通信都经过加密传输。这个过程出现过一次证书配置错误——没有把证书链文件包含进配置文件,导致部分客户端验证失败,修好后把所有主流的浏览器都测了一遍。
5.2 常见问题速查表
开发期间记录了大量问题排查经验,下面的表格是实际操作中最高频的问题和对应解决方案。每个问题都是我或者团队成员真实遇到过的,处理思路也经过了反复验证。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 前端请求接口返回 404 | 路由配置错误或接口路径不一致 | 查看浏览器 Network 面板请求 URL 对比后端路由 | 检查 @RequestMapping 注解路径和前端 API 地址是否一致 |
| 用户登录成功后接口仍返回 401 | Token 未正确传递或已过期 | 查看请求头中 Authorization 字段 | 检查前端拦截器是否设置了请求头,确认 Redis 中 Token 是否过期 |
| 上传图片后访问 403 | Nginx 静态目录权限不足 | 查看 Nginx 错误日志 | 调整目录权限为 www 用户可读,重载 Nginx 配置 |
| 店铺评分计算错误 | 定时任务未执行或加权逻辑有偏差 | 查看定时任务日志,检查评分数值 | 检查 cron 配置,验证加权公式的边界条件 |
| 数据库查询耗时突然变长 | 慢查询或缺少索引 | 开启慢查询日志,EXPLAIN 分析执行计划 | 根据执行计划添加合适索引,优化 SQL 写法 |
| 前端列表页白屏 | 接口返回数据结构变化前段未适配 | 查看浏览器控制台报错信息 | 检查后端返回的 data 结构,确认前端解析字段匹配 |
| 搜索关键词返回空结果 | 分词器未正确配置或数据未同步 | 检查 Elasticsearch 索引数据量 | 确认定时同步任务执行状态,检查 IK 分词词库 |
| 定时任务重复执行 | 多实例部署时定时任务冲突 | 查看多台服务器日志对比 | 引入分布式锁机制,或把定时任务独立成单独服务 |
5.3 排查思路与调试技巧
问题排查是一个逐步缩小的过程。我在实际操作中形成的思路是:先确认问题在哪个环节,再定位具体原因,最后验证修复方案。
一个典型的排查例子是用户反馈首页打开很慢。我先用浏览器的 Performance 面板看时间消耗,发现主要耗时在接口请求,而不是页面渲染。然后看接口响应时间,发现接口本身就有三秒多的延迟。再去看数据库日志,定位到是一条 SQL 查询走了全表扫描。最终给相关字段添加索引并优化了查询条件,接口响应时间从 3 秒以上降到了 300 毫秒以内。
调试技巧上,后端我强烈推荐在开发阶段开启 Swagger 接口文档,能直接在线测试接口,免去了反复构造请求参数的麻烦。前端调试时善用 Vue DevTools 查看组件状态和 Vuex 数据流,大部分前端状态问题都能在 DevTools 里直接定位。Redis 调试时用 redis-cli 命令行工具查看 key 和过期时间,比用各种 GUI 工具更直接高效。
6. 安全加固与用户体验细节
6.1 Web 安全防护实践
Web 安全在项目初期容易被轻视,但作为上线运行的系统,安全方案必须提前考虑。我重点处理了四个方面的问题,每个都是实际攻击场景里最常被利用的入口。
XSS 跨站脚本攻击的防护是通过前端框架自动转义加上后端输入过滤双重保障。Vue 默认会对模板中的变量进行 HTML 转义,大大降低了注入风险。后端在接收用户提交的富文本内容时,会过滤掉 script 标签和事件属性。用户头像、店铺封面这些图片 URL 的校验也很重要:必须校验协议只允许 http 和 https,防止 javascript: 伪协议的注入。
CSRF 攻击的防护因为使用了前后端分离模式,天然具备一定免疫能力。Token 通过请求头传递而不是 Cookie 自动携带,跨站请求无法自动带上 Authorization 请求头,所以 CSRF 风险显著降低。在商户管理相关的敏感接口上,额外做了来源校验,强制检查 Origin 请求头是否为受信任的域名。
登录相关的安全还做了额外的细节。用户登录接口增加了频率限制,同一个 IP 在一分钟内连续密码错误五次,就锁定十分钟。这种策略有效地防止了暴力破解。密码重置流程使用了时效性验证码,验证码有效期五分钟,并且重置后立即让所有已登录会话失效,防止密码被重置后旧会话仍然有效的问题。
6.2 关键页面交互优化
用户体验层面的细节,我在完成基础功能后就开始了打磨。移动端优先的响应式设计是第一个原则,因为美食探店场景天然具有移动属性,超过七成的用户通过手机访问。所以前端框架的栅格系统针对不同屏幕尺寸做了断点适配,手机端优先展示核心信息,平板和桌面端适当增加信息密度。
空数据状态的处理经常被忽视。店铺列表为空时,不能简单显示一行“暂无数据”,而是给用户一个明确的引导动作,比如跳转到热门店铺列表看看别人的探店笔记,或者提示可以切换一个菜系来发现更多美食。当用户只输入了一个不够明确的关键词时,系统会给出一些搜索建议,而不是直接返回空列表。
加载状态反馈也做了统一规范。页面初次加载用骨架屏,局部刷新用 loading 状态按钮,数据提交后用 toast 提示操作结果。请求超时和网络异常时不是白屏放那儿,而是显示一个友好的错误页,并提供“点击重试”按钮。这些细节不复杂,但对用户留存率的影响非常直接。
7. 项目心得与经验总结
整个项目从设计到落地,我最大的感受是:架构设计的决策不应该追求“最先进”,而应该追求“最匹配”。美食探店平台这种业务模型,本质上是一个内容社区加上同城生活服务的结合体,有的技术方案看着高大上,实际跑起来反而增加了不必要的复杂度。
具体来说,我在两个决策上特别庆幸当初的坚持。第一个是在数据库层面用上了精确的经纬度存储和联合索引,而不是直接往数据库里塞一个大字符串的 JSON 坐标字段。这让我在实现距离排序和附近推荐时少走了大量弯路。第二个是坚持在开发初期就引入 Redis 做缓存,而不等到系统被压垮了再临时补缓存层。缓存机制一旦确定,后续加接口都顺理成章,不用反复思考这个接口需不需要缓存。
踩过的坑也让我学到不少。最大的一个教训是:接口设计必须在前端开始写代码之前就完全确定下来,哪怕慢两三天也值得。我们当初一边写代码一边改接口,导致前期大量联调时间浪费在适配反复修改的字段上。后来强制要求后端先把所有接口文档写好,前端和后端各自开发,联调效率明显提升。
最后分享一个做部署的小技巧。上线那天不要选择在业务高峰期,我习惯在凌晨发布,因为即使出了问题,影响面也最小。发布前把旧版本的 jar 包和数据库备份都留着,万一新版本出问题可以快速回滚。另外,日志是运维时的眼睛,我在项目里统一采用了 Logback 日志框架,日志按日期滚动归档,线上排查问题时能快速定位到当天的错误日志。
连续运行了三个月,这个平台在日常使用中表现稳定,核心服务的使用体验也达到了预期目标。项目本身还会继续迭代,后续优先要做的事情包括接入更智能的个性化推荐算法、增加基于地理位置的实时探店动态、优化移动端体验。做这样一整个系统的收获是全面的,从技术选型、架构设计到实际编码、部署上线,每一步都踏踏实实走过来,希望这些经验对你也有帮助。
