ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Spring Boot+Vue的文创内容推荐系统设计与实现

基于Spring Boot+Vue的文创内容推荐系统设计与实现

1. 文创内容推荐这个方向,到底难在哪里

前阵子帮一个学弟看他的毕业设计选题,他提了一个挺有意思的需求:做一个热门文创内容的推荐平台。当时我第一反应是"这有什么难的",数据库几张表、后台管内容、前端轮播图加列表页,一个标准的CRUD系统而已。但等他把自己收集的需求文档发给我,我才意识到文创内容推荐这个方向还真不像看上去那么轻量。

先说场景。文创内容这个品类很特殊,它不像3C数码那样参数驱动,也不像纯资讯那样时效驱动。用户买一个博物馆文创帆布袋,可能是因为某个IP联名;收藏一本手账贴纸,可能是因为这套画风符合自己的审美;下单一个国潮茶杯,可能是送礼场景下的临时决策。换句话说,文创内容是典型的"兴趣驱动 + 审美驱动 + 场景驱动"内容,用户自己都说不清楚自己要什么,只会说"我再逛逛"。

正因为这种特性,市面上大部分通用电商平台或者内容社区在文创这个垂类上体验都不够好。泛推荐给用户推来的全是爆款,用户两天就腻了;纯人工编辑虽然质量稳定,但没法规模化。这时候一个基于Spring Boot + Vue的轻量级推荐系统就有了清晰的定位:它不追求大而全,而是通过标签体系、用户行为采集、推荐排序算法,在小体量数据下实现"比用户自己更懂他想逛什么"的体验。

这篇博文我打算把这个项目从零到一完整拆开讲一遍,包括数据模型怎么设计、推荐逻辑怎么做才不过度设计、前后端接口怎么约定、源码结构怎么才适合答辩讲解、部署文档应该写到什么颗粒度。如果你正在做类似方向的设计,或者接手了一套"源码+设计文档+部署文档+讲解"形式的项目需要二次开发,这篇文章应该能帮你少走不少弯路。

先说清楚:我讲的不是那种动不动就上Spark、Elasticsearch、深度学习的大平台方案,而是适合教学演示、答辩展示和个人二次开发的工程化实现路线。稳定的、能跑的、逻辑清晰的,才是这个项目最该追求的目标。

2. 技术选型的取舍逻辑:为什么Spring Boot + Vue是当前最稳妥的组合

很多人在选题阶段会卡在技术栈上,纠结要不要上微服务、要不要用Python写推荐算法再让Java调用、前端要不要用微前端架构。我的建议很直接:除非你的项目背景明确要求了分布式和高并发,否则Spring Boot + Vue这种经典前后端分离组合,就是这个场景下性价比最高的选择。

2.1 后端选型:Spring Boot承担什么职责

后端我用的是Spring Boot 2.7.x,搭配MyBatis-Plus作为ORM框架。为什么是2.7而不是3.x?主要考虑的是生态兼容性。很多现成的脚手架、依赖、教程都是基于2.x系列的,而且2.7是2.x生命周期里最成熟的版本,对教学项目来说稳定压倒一切。

Spring Boot在这个项目里有四个核心职责:

第一是提供RESTful API。文创内容的展示、用户行为的上报、推荐结果的获取,全部走HTTP接口。前端不关心后端是Java还是Python,只要接口契约稳定就行。

第二是业务逻辑编排。推荐算法虽然在数据库层面可以通过SQL实现一部分,但真正的排序策略、加权计算、冷启动兜底还是要放在Service层做。

第三是数据访问控制。MyBatis-Plus的Wrapper机制可以让我们少写大量XML,简单的单表查询直接链式调用,复杂一点的联表查询再手写SQL,这种组合非常舒服。

第四是权限与安全。JWT登录鉴权、管理员接口保护、用户密码加盐加密,这些都需要后端统一处理,不可能指望前端做安全控制。

2.2 前端选型:Vue 3 + Element Plus还是Vue 2 + Element UI

前端框架我建议分情况看。如果你是2024年之后才开始学Vue,直接上Vue 3 + Vite + Element Plus,不要犹豫。Vite的冷启动速度快到让人感动,组合式API写业务代码也顺手。但如果你拿到的项目骨架是Vue 2 + Element UI,也别急着重构——在毕业设计答辩这个场景下,框架新旧不是决定项,功能完整度和逻辑清晰度才是。

我自己写这个项目时用的是Vue 3。页面包括:用户端的内容瀑布流首页、分类筛选页、详情页、个人中心、收藏夹,管理端的文创内容管理、标签管理、用户行为统计、推荐参数配置。用Vue Router做页面路由,Pinia做全局状态管理,Axios统一封装请求。

这里有一个很多新手会忽略的点:页面容易做,状态管理容易乱。用户登录之后token存哪里?推荐页切换Tab的时候筛选条件要不要保留?收藏和取消收藏的按钮状态如何和列表页联动?这些问题在编码之前如果不想清楚,写一半就会陷入"改一个变量到处报错"的窘境。

2.3 数据库与中间件选型:MySQL + Redis的组合边界

数据存储用MySQL,这个没有悬念。Redis在什么场景下用要讲清楚:我不建议把推荐结果完全缓存到Redis,因为数据量小、计算逻辑简单,每次实时算也很快;但适合把"热门榜单Top50"这种全局性结果缓存5分钟,避免每次请求都去查全表。另外验证码的存储、JWT黑名单、用户登录状态,这些天然适合Redis。

还有一个常见的过度设计倾向:引入消息队列、Elasticsearch、MongoDB。这些工具本身没问题,但不是这个项目的必需品。如果你的设计文档里写了Kafka,但实际代码里只拿它打印了日志,答辩时老师问一句"为什么这里要用消息队列"就容易卡壳。技术选型要能解释得通逻辑闭环。

3. 数据模型设计:文创产品、用户行为与标签体系的建模

这个项目的数据模型比起一般的管理系统要稍微复杂一点,核心原因是"推荐"依赖数据,数据不好推荐逻辑就是空中楼阁。我画表的时候是围绕三条主线展开的:一是内容本身的分类与属性,二是用户行为轨迹,三是标签关系网络。

3.1 核心表结构与设计思路

下面是我实际建表时用到的核心表,每张表都尽量精简但字段不缺失。

表名用途关键字段设计
user用户信息id, username, password, nickname, avatar, role
category文创分类id, name, sort, description
item文创内容主表id, title, cover, images, summary, detail, category_id, status, view_count, like_count, favorite_count
tag标签表id, name, type
item_tag_relation内容-标签关联表item_id, tag_id
user_behavior用户行为记录id, user_id, item_id, behavior_type, create_time
user_favorite收藏表id, user_id, item_id, create_time
recommend_config推荐权重配置表id, view_weight, like_weight, favorite_weight, time_decay_rate

两张关系表(item_tag_relation、user_behavior)是这个项目区别于普通CRUD的关键。标签关系表让内容可以被多维描述,比如一件文创T恤可以同时挂"国潮""联名""服饰"三个标签;用户行为表则是推荐算法的数据原料,用户的每一次浏览、点赞、收藏都会沉淀成一条记录。

3.2 文创内容的多维属性怎么建模

文创内容和普通商品最大的区别在于"描述维度丰富"。一件故宫文创笔记本,它有品类属性(本册)、风格属性(复古)、IP属性(故宫)、价格带属性(100元以内)、人群属性(学生党/白领)。如果只用一张表的若干个字段去描述,后期扩展性极差。

所以我采用了"主表 + 标签关联表"的模式:通用属性放在item主表,垂直属性通过tag表动态扩展。这样管理员上传内容时只需要勾选已有的标签,不需要修改表结构。构建推荐逻辑时,标签作为计算因子也更容易处理。

标签表的type字段用来区分标签维度,比如style代表风格,scene代表使用场景,ip代表IP联名。推荐算法在做内容相似度计算时,可以按维度加权,而不是把标签全部一视同仁。

3.3 用户行为的采集粒度:太粗没有价值,太细存储爆炸

用户行为表的设计有个需要拿捏的粒度问题。我试过把"停留时长""鼠标滚动深度"都记录进去,后来发现这些数据在这个体量的项目里根本用不上,反而让代码复杂度成倍上升。

最终只保留了三种行为类型:view(浏览)、like(点赞)、favorite(收藏)。这三种行为能表达用户意愿强度递进:浏览是弱意愿,点赞是中等意愿,收藏是强意愿。推荐算法计算兴趣分数时,不同类型行为赋予不同权重即可。这个设计既能支撑推荐逻辑,又不会因为过度采集让系统显得臃肿。

行为表要建索引,这里特别注意:联合索引(user_id, item_id)和单列索引(item_id)都要建。否则推荐逻辑里"根据用户历史行为找相似内容"这种查询,在数据量几百条感觉不到慢,上万条之后性能差距就很明显了。

4. 推荐逻辑落地:从热度榜到个性化排序的三阶段演进

推荐模块是这个平台的心脏,也是最容易写得故弄玄虚的地方。很多同学一提到推荐算法就想到协同过滤、矩阵分解、深度神经网络,然后陷入"数学公式看不懂、代码写不出、答辩说不清"的困境。

我的建议是把推荐逻辑做成三阶段递进,每一阶段都有明确的工程实现,整体上层层递进:先保证内容质量,再做个性化,最后再做内容发现。

4.1 第一阶段:热度排序,保证基础体验

最初的版本不需要任何个性化,按热度排序即可。热度分我采用多指标加权加时间衰减,具体公式:

hot_score = (view_count * 1 + like_count * 3 + favorite_count * 5) / (hours_since_publish + 2)

这个公式的逻辑是:

  • 浏览、点赞、收藏分别代表不同强度的用户认可,权重按1:3:5递增
  • 除以"发布时间距现在的小时数 + 2"是为了让新内容有机会浮上来
  • 加2是平滑项,防止刚发布的内容因为分母为0导致分子爆炸

用MySQL可以直接写成SQL计算,也可以在Java内存里一次加载后排序。对于几百条数据,内存排序完全够用。

这个阶段能解释清楚一个核心问题:为什么不直接按浏览量排序?因为有时间衰减的热度能避免老内容霸榜,让新上架的文创产品也能进入用户视野。

4.2 第二阶段:标签匹配的内容推荐,让推荐"开始懂人话"

热度排序的缺陷是所有人看到的都一样,所以第二阶段引入基于标签的内容推荐,也叫基于内容的推荐(Content-Based Filtering)。

核心思想:用户之前喜欢什么,就推荐和这些内容标签相似的内容。

实现步骤:

  1. 从user_behavior表中找出当前用户已收藏或已点赞的item_id列表
  2. 根据这些item_id查询item_tag_relation,统计出用户的"标签偏好分数"
  3. 对所有其他item计算标签重合度
  4. 按重合度排序,重合度相同再用热度分兜底

标签偏好分数的计算可以很简单:用户每收藏一个内容,该内容关联的所有标签分数加1;每点赞一次加0.5;每浏览一次加0.2。累加得出每个标签的偏好分。

查询代码用MyBatis-Plus可以这么写:

// 用户标签偏好计算 List<UserBehavior> behaviors = userBehaviorMapper.selectList( new LambdaQueryWrapper<UserBehavior>() .eq(UserBehavior::getUserId, userId) .in(UserBehavior::getBehaviorType, Arrays.asList("like", "favorite")) ); Map<Long, Double> tagScoreMap = new HashMap<>(); for (UserBehavior behavior : behaviors) { List<ItemTagRelation> relations = itemTagRelationMapper.selectList( new LambdaQueryWrapper<ItemTagRelation>() .eq(ItemTagRelation::getItemId, behavior.getItemId()) ); for (ItemTagRelation relation : relations) { double score = "favorite".equals(behavior.getBehaviorType()) ? 1.0 : "like".equals(behavior.getBehaviorType()) ? 0.5 : 0.2; tagScoreMap.merge(relation.getTagId(), score, Double::sum); } }

然后根据这个tagScoreMap去匹配未被用户看过的内容,按重合度排序。

这里有个细节:不要把用户已经看过的内容再推荐一遍。所以在查询候选集时,要用NOT IN过滤掉行为表里出现过的item_id。

4.3 第三阶段:协同过滤思路的简化实现

第三阶段是协同过滤的简化版本,核心思路是"和你口味相似的人也在看什么"。完整的协同过滤要计算用户相似度矩阵,在小项目里没必要做这么重。

我的做法是做一个基于记忆的简化版:

  1. 找到和当前用户有相同收藏行为的其他用户
  2. 收集这些用户收藏过但当前用户没收藏过的内容
  3. 按"被多少个相似用户收藏过"排序,取TopN

SQL可以这样写:

SELECT item_id, COUNT(DISTINCT user_id) AS user_count FROM user_behavior WHERE behavior_type = 'favorite' AND item_id NOT IN ( SELECT item_id FROM user_behavior WHERE user_id = #{currentUserId} AND behavior_type = 'favorite' ) AND user_id IN ( SELECT DISTINCT user_id FROM user_behavior WHERE behavior_type = 'favorite' AND item_id IN ( SELECT item_id FROM user_behavior WHERE user_id = #{currentUserId} AND behavior_type = 'favorite' ) ) GROUP BY item_id ORDER BY user_count DESC LIMIT #{limit}

这个SQL的逻辑是:先找到和我收藏过同一件商品的人,再看看他们还收藏了什么其他商品,然后统计这些商品被多少人收藏过。

这三个阶段在代码里是三个Strategy,可以根据推荐参数配置来决定用哪个。初次访问的用户没行为数据,走热度排序;有少量行为数据走内容推荐;行为足够多走协同过滤。这就是冷启动问题的朴素解法。

5. 核心接口设计与安全控制:前后端协作的约定

接口设计是否规范,直接决定了前端开发效率以及后续答辩时老师对项目质量的印象。一个接口混乱的项目,哪怕功能完整,也会被一眼识破是拼凑出来的。

5.1 统一响应结构与分页约定

所有接口返回统一结构,这是前后端协作的基础。

{ "code": 200, "message": "success", "data": { "records": [], "total": 128, "current": 1, "size": 10 } }

后端用Result 泛型类统一包装,前端Axios响应拦截器统一判断code,不等于200时统一ElMessage报错。这样前端不需要每次请求都写一遍错误处理。

分页参数统一用current和size,后端用MyBatis-Plus的Page对象接收,避免出现pageNum、limit、pageSize等参数混用的情况。

5.2 推荐相关核心接口

项目里最核心的几个接口:

接口路径方法说明
/api/recommend/feedGET首页推荐流,根据用户行为和推荐策略返回内容列表
/api/recommend/similar/{itemId}GET详情页"相关推荐",基于当前内容标签找相似内容
/api/item/{id}GET内容详情,包含浏览量自增
/api/behavior/reportPOST用户行为上报,前端埋点
/api/favorite/togglePOST收藏/取消收藏
/api/admin/itemsGET/POST/PUT管理员内容管理接口

这里重点说一下行为上报接口的设计,因为很多项目会在这里栽跟头:

@PostMapping("/api/behavior/report") public Result<Void> reportBehavior(@RequestBody BehaviorReportDTO dto) { // 参数校验 Assert.notNull(dto.getItemId(), "itemId不能为空"); Assert.hasText(dto.getBehaviorType(), "behaviorType不能为空"); // 防重复提交:同一用户对同一内容同一行为,限制频率 String redisKey = "behavior:" + userId + ":" + dto.getItemId() + ":" + dto.getBehaviorType(); Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", 10, TimeUnit.SECONDS); if (Boolean.FALSE.equals(absent)) { return Result.success(); } // 异步保存行为,避免影响主流程响应 UserBehavior behavior = new UserBehavior(); behavior.setUserId(userId); behavior.setItemId(dto.getItemId()); behavior.setBehaviorType(dto.getBehaviorType()); behaviorMapper.insert(behavior); return Result.success(); }

两个细节:防重复提交用Redis的setIfAbsent加10秒过期;行为保存只对收藏和点赞做实时处理(因为收藏状态要马上反馈到UI),浏览行为可以直接异步落库或干脆只在Redis里计数、定期批量写入。

5.3 JWT鉴权与权限控制

前端用户和管理员的接口要区分开。我的做法是:

  • 用户登录接口 /api/auth/login,成功后返回JWT token
  • 前端把token存在Pinia + localStorage,Axios请求拦截器自动加Authorization头
  • 后端用拦截器解析token,把userId放到ThreadLocal上下文
  • 管理员接口额外检查role,不是admin直接403

登录密码存储必须用BCrypt加密,很多人图省事用MD5,这在答辩时是硬伤。BCrypt的特点是同密码不同盐、结果不同、无法反查,Spring Security里的BCryptPasswordEncoder直接拿来用就行。

// 注册时加密 String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword); // 登录时校验 boolean matched = new BCryptPasswordEncoder().matches(rawPassword, user.getPassword());

5.4 管理端推荐参数配置接口

项目里加了一个管理端配置推荐权重的功能,对应recommend_config表。管理接口允许管理员调整浏览、点赞、收藏的权重值以及热度时间衰减速率,这个设计在答辩时非常加分——它把推荐系统从"写死的代码"变成了"可配置的运营工具",能体现出你对业务运营的理解。

6. 交付物统筹:源码结构、设计文档、部署文档与讲解视频怎么组织才成体系

这个项目标题里带了"源码+lw+部署文档+讲解",也就是说交付的不只是能跑通的代码,而是一整套可复现、可学习、可答辩的项目资料。这一部分很多同学不重视,等到要交的时候才发现代码和文档对不上,或者部署文档缺步骤导致环境搭不起来。

6.1 源码目录结构设计:让审阅者一眼找到核心代码

后端我采用标准的分层结构:

backend/ ├── src/main/java/com/example/creative/ │ ├── controller/ # 接口层 │ │ ├── AuthController.java │ │ ├── RecommendController.java │ │ ├── ItemController.java │ │ └── AdminController.java │ ├── service/ # 业务逻辑层 │ │ ├── RecommendService.java │ │ ├── UserBehaviorService.java │ │ └── ItemService.java │ ├── mapper/ # MyBatis-Plus数据访问层 │ ├── entity/ # 实体类 │ ├── config/ # 配置类(跨域、拦截器、Redis) │ ├── common/ # 统一返回结构、异常处理 │ └── strategy/ # 推荐策略(热度/内容/协同) └── src/main/resources/ ├── application.yml └── mapper/ # 手写SQL的XML文件

strategy包是我精心设计的,推荐策略用策略模式封装,新增推荐算法只需要实现一个接口再注册到容器里,这个设计点在答辩中非常出彩。目录结构本身就是代码质量的一部分。

前端目录:

frontend/ ├── src/ │ ├── api/ # 接口请求封装 │ │ ├── user.js │ │ ├── item.js │ │ └── recommend.js │ ├── views/ │ │ ├── home/ # 推荐首页 │ │ ├── category/ # 分类浏览 │ │ ├── detail/ # 内容详情 │ │ ├── profile/ # 个人中心 │ │ └── admin/ # 管理后台 │ ├── components/ # 公共组件 │ ├── router/index.js # 路由定义 │ ├── store/ # Pinia状态管理 │ └── utils/request.js # Axios封装 └── vite.config.js

6.2 设计文档怎么写:不是代码说明书,是设计推导过程

很多同学把设计文档写成了代码粘贴本,一贴就是几百行,评审老师根本不想看。设计文档的正确写法是呈现"为什么这么做"的逻辑链。

我的经验是把这个项目的设计文档分成六章:

  1. 项目背景与需求分析:文创内容推荐的行业背景、用户痛点
  2. 技术选型分析:为什么选用Spring Boot + Vue,数据存储方案对比
  3. 系统设计:整体架构图、功能模块划分、数据库设计(ER图加核心表字段说明)
  4. 核心算法设计:推荐算法的3个阶段公式、推导过程
  5. 核心功能实现:每个功能模块的关键代码片段加解释
  6. 系统测试与部署:接口测试用例、性能测试结果、部署步骤

写设计文档的时候有个技巧:每一个设计决策后面跟一个"为什么"。比如"为什么用Redis缓存热门榜单?因为热门榜单的实时性要求不高、计算成本相对高,缓存5分钟可以显著降低数据库压力"。这种解释比堆代码有价值得多。

6.3 部署文档的颗粒度:环境照做就一定能跑起来

部署文档最重要的指标是"照着做一定能成功"。我写过太多部署文档,回头查问题发现是自己漏写了一个环境变量或版本要求。

部署文档要包含的内容:

  • 环境要求:JDK版本(1.8或11)、MySQL版本(5.7或8.0)、Redis版本、Node版本、npm源
  • 数据库初始化:初始化SQL位置、运行方式、需要手动修改的配置项(数据库密码)
  • 后端部署:修改application.yml中的数据库连接、Redis连接、启动命令
  • 前端部署:npm install注意事项、npm run build、dist目录位置
  • Nginx配置:静态资源托管路径、API反向代理、history路由rewrite
  • 常见错误对照表:连接失败、端口占用、404错误、白屏等

给每个步骤配上运行成功的预期结果截图,这段内容是最费功夫但也是最容易被评审认可的。

6.4 讲解视频和答辩的思路组织

讲解视频不用太长,控制在15到20分钟。我的安排是:

  • 开场1分钟:一句话说明项目定位
  • 3分钟:演示用户端核心页面(首页推荐、详情、收藏、个人中心)
  • 2分钟:演示管理端功能(内容管理、标签管理、推荐配置)
  • 5分钟:讲解核心代码(推荐算法的实现逻辑、接口设计)
  • 3分钟:演示部署流程(本地启动前后端、环境变量说明)
  • 1分钟:总结项目亮点和可扩展方向

答辩时容易被问的问题提前准备:推荐算法为什么不用机器学习?协同过滤的数据稀疏问题怎么解决?假如数据量达到千万级你的系统要怎么演进?这些问题没有标准答案,但提前想清楚会让答辩过程非常从容。

7. 部署上线实战:从本地联调到服务器稳定运行

最后一部分是部署实战,这部分踩坑最多,我详细拆解一下全流程。

7.1 本地环境准备与前后端联调

本地开发我用的环境是:Windows + JDK 1.8 + MySQL 5.7 + Redis 6 + Node 16。建议用Maven管理后端依赖,npm管理前端依赖。

启动顺序要注意:

  1. 先启动Redis,确认端口6379正常监听
  2. 导入数据库脚本,修改application.yml里的数据库账号密码
  3. 启动后端Spring Boot应用,确认8080端口启动没有报错
  4. 浏览器直接访问后端Swagger地址,验证接口
  5. 启动前端npm run dev,访问localhost:5173

前后端联调最容易出的问题是跨域。开发环境下我通过后端CorsConfig配置允许跨域,生产环境则完全交给Nginx反向代理,由同源策略规避跨域问题。Vite开发服务器默认端口是5173,后端允许的来源要写正确:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

7.2 服务器部署流程拆解

生产服务器我用的是CentOS系统的云主机,部署流程如下:

第一步:安装环境。

# 安装JDK yum install java-1.8.0-openjdk-devel # 安装MySQL yum install mysql-server # 安装Redis yum install redis # 安装Nginx yum install nginx # Node.js可以通过nvm管理 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

这里提醒一句:服务器上的MySQL一定要执行安全初始化,设置root密码、关闭远程root登录。Redis如果不对外提供服务,bind 127.0.0.1并关闭protected-mode,这些配置一步都不能省。

第二步:打包部署后端。

# 本地确认代码没问题后执行打包 mvn clean package -DskipTests # 上传jar包到服务器,后台启动 nohup java -jar creative-recommend-1.0.0.jar \ --spring.profiles.active=prod \ > /app/logs/app.log 2>&1 &

查看日志用tail -f /app/logs/app.log,确认出现"Started Application in xx seconds"就代表启动成功。

第三步:构建前端并配置Nginx。

# 本地构建 npm run build # 上传dist目录到服务器 /app/nginx/html/creative

Nginx配置如下:

server { listen 80; server_name your_domain; root /app/nginx/html/creative; index index.html; # 前端路由history模式,刷新不404 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { 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; } }

配置完执行nginx -t测试语法,然后nginx -s reload生效。

7.3 部署阶段的高频故障和排查方法

部署阶段我实测遇到最多的问题就五个,我列成表方便对照排查:

问题现象可能原因排查命令解决办法
后端启动失败端口被占用netstat -tlnp | grep 8080kill占用进程或改端口
数据库连接失败账号密码错误检查application.yml更新数据库凭据
前端页面白屏静态资源路径不对查看Nginx error.log修改root路径或绝对路径引用
接口404Nginx proxy_pass路径拼接问题curl -v http://127.0.0.1:8080/api/xxx确认后端context-path配置一致
刷新页面404Nginx没配try_files直接查看Nginx配置加上try_files $uri $uri/ /index.html

部署文档里把这五条放进去,基本覆盖90%的部署故障场景。

8. 开发这个项目时我踩过的几个坑

这个项目从开发到部署,我踩过的坑挺多,挑几个印象最深的讲,这些都是源码里不会写、文档里也不会提的实战经验。

第一个坑在用户行为统计的重复数据。前端Vue组件在页面加载时触发行为上报,但Vue的组件生命周期在开发模式下会执行两次(这是因为严格模式故意的),导致同一浏览行为被记录了两遍,收藏数明显虚高。解决方式除了前端做埋点去重,后端也要用Redis做幂等控制。两边都做才能彻底防住。

第二个坑在逻辑删除和唯一索引的冲突。我给user_favorite表设置了联合唯一索引(user_id, item_id),后来给所有表加了MyBatis-Plus的逻辑删除字段status=1表示正常、status=0表示删除。问题来了:用户取消收藏后逻辑删除记录,然后再次收藏,唯一索引不冲突(因为删除的记录还在表里),但查询时逻辑删除的数据被自动过滤了,导致收藏状态判断错误。解决方式是唯一索引改成(user_id, item_id, status)三联唯一,或者取消逻辑删除直接用物理删除。推荐走物理删除,收藏记录不是需要审计的敏感数据。

第三个坑是JSON序列化导致的时间格式问题。Java后端返回的LocalDateTime默认会被序列化成数组格式[2024, 3, 15, 14, 30, 25],前端展示非常痛苦。需要在application.yml里配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

第四个坑是我在联调个性化推荐时发现的:基于内容推荐的候选集里包含了用户已经收藏过的内容,导致推荐页反复出现同一个内容。原因是NOT IN子查询漏掉了like行为,只过滤了favorite。这个问题的教训是:推荐算法的候选集过滤条件一定要把所有历史行为都算进去,不只是强意愿行为。

第五个坑是Nginx部署时前端图片资源404。原因是根路径被配置在/home/ubuntu/project/frontend/dist,但Nginx进程的运行用户权限不够无法读取该目录。解决方式要么给目录加执行权限,要么把构建产物放到Nginx默认的html目录下,我后来采用后者,干净利落。

写在最后

这个项目做完之后最大的感受是:推荐系统的核心不在算法有多高级,而在于数据模型是否支撑得起推荐逻辑、推荐结果是否能解释得清楚。用Spring Boot + Vue做一个文创内容推荐平台,技术上完全可行,而且工程化的每一步都能在答辩中讲出实质内容。

最后再分享一个小技巧:如果你准备把这个项目作为毕业设计或者面试作品,一定要把推荐策略模式的设计讲透,以及把用户行为采集的防重复机制讲清楚。这两个点足以让听的人感受到你不只是会把代码跑通,而是真的在思考工程问题。

项目的后续扩展方向我也顺带提一下,比如接入更细致的用户画像、增加定时任务定期计算用户偏好快照、推荐结果的A/B测试模块。这些方向都在现有架构上都有合理的位置,但至少当前这个版本,已经能够稳稳支撑起一个完整、可演示、可复盘的文创推荐平台了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进