☰
非遗平台推荐系统落地实践:SpringBoot+Vue微服务与协同过滤
2026/10/3 9:08:08 网站建设 项目流程

去年我接了一个非遗数字化平台的项目,需求文档里最扎眼的一句话是:“能不能让用户逛完一个非遗专题后,自动给他推相关的非遗项目?”一开始我还天真地想,做个推荐位,按地区或类别筛选一下不就行了。结果业务方又补了几条要求:平台要支持微信小程序、App和运营后台;推荐结果不能只按热度排,要“像回事”;管理端要有一个可视化大屏,直接看到全国非遗分布和用户行为。这几个需求凑在一起,单体应用很难收拾,最后我们选型为SpringBoot+Vue+SpringCloud,搭了一套微服务架构,在推荐服务里实现了协同过滤算法,再用ECharts做了可视化大屏,用MinIO和M3U8解决了非遗视频资源的存储与在线播放。这套方案从设计到上线大概花了四个月,这篇把思路和踩过的坑完整记录下来。

1. 非遗推荐系统的业务模型:先搞清楚数据长什么样,再谈算法和微服务

1.1 非遗数据的天然特殊性

做非遗推荐,和做电商推荐最大的区别在数据形态。电商商品有清晰的类目、规格、价格、销量,用户行为也有明确的转化目标。非遗数据就不一样了:一个非遗项目往往包含了项目名称、级别(国家级、省级)、所属类别(传统技艺、传统美术、民俗、传统戏剧等)、申报地区、传承人信息、项目简介、图片、视频资源,甚至还有历史典故和工艺流程。同一个“苏绣”项目,在不同地区的申报材料、视频讲解、传承人故事可能完全不同。

这意味着推荐系统的数据基础不是简单的“商品ID”,而是一堆半结构化内容。我们最初的数据库设计只建了一张heritage表,把简介、图片、视频URL全部塞进去,结果后续做标签提取和相似度计算时苦不堪言。后来重新梳理了数据模型,把非遗项目主表、传承人表、非遗视频表、非遗标签表、用户行为表分开。标签表是重点,因为协同过滤算的是“用户-项目”的交互,但冷启动的时候必须依赖项目自身的内容标签,后面会详细说。

1.2 用户行为采集是整个推荐系统的地基

协同过滤的核心是用户行为数据。没有行为数据,算法再花哨也是空的。我们当时梳理出的关键行为包括:

  • 浏览:用户在非遗详情页停留时长、是否完整看完介绍。
  • 收藏:用户主动点击“收藏”按钮。
  • 点赞:对视频、文章、传承人故事点赞。
  • 搜索:用户搜索了“剪纸”“皮影”“古琴”等关键词。
  • 观看:视频类的非遗项目,用户播放、暂停、拖动进度。

采集方式不能只靠前端上报。我们做了一个统一行为埋点接口,接入Spring Cloud Gateway网关,通过Post请求把行为日志发到用户服务。用户服务先校验登录态,然后把原始行为写入RabbitMQ消息队列,再由日志消费服务异步落库。为什么不直接同步写入MySQL?因为行为数据量大,而且高频,同步写入会拖垮主流程,尤其是视频观看进度这类数据,用户每几秒就上报一次,同步写库非常不划算。

落库后的行为表结构大概是这样的:user_id、heritage_id、behavior_type、score、duration_seconds、create_time。其中score是我们自定义的隐式反馈分数。比如浏览得1分,收藏得5分,点赞得3分,搜索到并点击得4分。之所以给行为赋分,是为了让协同过滤计算的矩阵不是全0/1,这样相似度区分度更高。

1.3 为什么推荐系统和微服务必须绑定

最开始我也有疑虑:推荐模块单独做成一个服务,是不是过度设计?单体应用里写个相似度计算接口不也行吗?后来发现不行。

协同过滤的离线计算是个重活。如果推荐用户量变大,我们要把用户行为矩阵读出来,计算物品相似度矩阵,然后定时更新推荐结果。这个计算任务如果放在单体应用进程里,一个Full GC就能让整个系统卡死。而且推荐服务的在线接口QPS并不高,但离线任务是CPU密集型的,两者放在同一个服务,互相影响。拆分之后,推荐服务可以独立部署,离线任务用单独的线程池跑,在线接口走Redis缓存,互不干扰。

另外,推荐系统的调用量会随着前端大屏、App首页、运营后台推荐管理多个端同时使用。把推荐服务独立出来,以后就算换推荐引擎,也不用动其他业务代码。所以我们最终确定,微服务不是“为了分布式而分布式”,而是因为推荐模块的资源特征和业务模块差异太大。

2. 微服务拆分与技术选型:SpringBoot+Vue+SpringCloud的模块边界

2.1 单个服务拆到什么粒度

我们按业务域拆成了这些服务:

  • 用户权限服务:登录、注册、角色权限管理,集成Spring Security + JWT。
  • 非遗项目服务:非遗项目的CRUD、分类、地区、传承人、标签管理。
  • 推荐服务:协同过滤计算、推荐接口、冷启动推荐、热门补救。
  • 统计服务:用户行为聚合、热度统计、大屏数据接口。
  • 文件服务:图片、视频上传,对接MinIO对象存储。
  • 网关服务:基于Spring Cloud Gateway做路由、鉴权、限流。
  • 后台管理服务:运营人员使用的后台管理系统API。

这个拆分粒度是综合考虑后定的。比如“文件服务”独立出来,是因为非遗的视频和图片资源很多,单独做上传、转码、访问控制比较合适;“统计服务”独立,是因为大屏要的数据要聚合多个服务,如果统计逻辑散在各个服务里,前端要调五六个接口才能拼出大屏数据。统计服务统一做聚合,前端只调一个接口,省事很多。

2.2 技术栈清单和版本陷阱

模块选型说明
注册中心Nacos 2.x同时做配置中心
网关Spring Cloud Gateway基于WebFlux,不能和Servlet混用
服务调用OpenFeign声明式HTTP客户端
熔断限流Sentinel网关和部分核心接口接入
ORMMyBatis-Plus简化单表CRUD
缓存Redis推荐结果缓存、用户会话
消息队列RabbitMQ行为日志异步处理
对象存储MinIO图片和视频文件存储
搜索Elasticsearch非遗项目全文检索
前端Vue3 + Element Plus + ECharts可视化大屏和后台

版本这里一定要提醒:SpringCloud和SpringBoot有严格的版本对应关系。我们当时用了SpringBoot 2.7.x,对应的SpringCloud版本是2021.0.x,也就是Jubilee。如果你乱配SpringBoot 3.x和旧版SpringCloud,服务启动直接报错。网上很多教程讲的是旧版本,看的时候一定要先确认版本号。另一个坑是Spring Cloud Gateway基于Netty,属于WebFlux体系,如果项目里不小心引入了spring-boot-starter-web,网关启动时会出现冲突,我们踩过一次,花了一晚上才定位到是依赖冲突。

2.3 服务间调用链设计

前端请求进来,先走网关路由到具体服务。

用户在小程序端打开首页,网关识别到访问的是产品服务路径,将请求转发到非遗项目服务;首页同时要展示“猜你喜欢”,网关将请求转发到推荐服务,推荐服务内部调用非遗项目服务获取项目详情,再从Redis里取推荐ID列表,最后拼装结果返回。用户点击进入某一个非遗项目详情,前端会调用行为埋点接口,把行为异步写入消息队列。

这里有一个很关键的调用链设计:推荐服务不要每次都去调非遗项目服务。我们要求推荐服务只返回heritage_id列表,前端拿到ID后再调非遗项目服务批量获取详情,或者在网关层做一次聚合。一开始我们图省事,让推荐服务直接调用非遗服务拉详情,结果推荐服务一旦脑裂,大量重复请求把非遗服务打挂了。后来改成推荐服务只维护ID和基础标题,完整详情统一由前端发起二次请求,系统稳定了很多。

3. 协同过滤算法落地:ItemCF与冷启动方案的组合拳

3.1 为什么不用深度学习,选了协同过滤

当时团队里也有人提议用Wide&Deep或者Graph Embedding,说效果更好。但实际情况是:平台上线初期用户量很少,行为数据稀疏,深度学习模型根本训练不起来。而且非遗推荐有个特点是解释性很重要,用户被推荐了“古琴艺术”,产品经理希望页面能告诉用户“因为你看过‘昆曲’,而‘古琴艺术’和‘昆曲’同属传统表演艺术,并且很多用户同时喜欢”。ItemCF天然可以给出这种解释。

所以我们最终选择了基于物品的协同过滤(ItemCF),辅以基于内容的冷启动推荐。这个方案在数据量不大时足够稳定,而且实现简单、算得快。

3.2 ItemCF核心实现

ItemCF的基本逻辑分三步:

  1. 根据用户行为,计算每一个用户对不同非遗项目的评分。
  2. 利用评分矩阵,计算非遗项目之间的相似度。
  3. 用户访问推荐接口时,根据该用户偏好的项目,找出相似项目,生成TopN推荐结果。

相似度计算我们用了改进的余弦相似度,乘上一个热度惩罚因子。为什么要惩罚热门项目?因为像“春节”“中秋”这类大众认知度极高的项目,和所有项目都有共现,相似度会被拉高,推荐出来全是热门非遗,失去个性化意义。

public class ItemCF { // userId -> heritageId -> score private Map<Long, Map<Long, Double>> userData; // heritageId -> similar heritageId -> similarity private Map<Long, Map<Long, Double>> similarityMatrix; public Map<Long, Double> recommend(Long userId, int topN) { Map<Long, Double> userScores = userData.getOrDefault(userId, Collections.emptyMap()); Map<Long, Double> recommendScores = new HashMap<>(); Map<Long, Double> simTotal = new HashMap<>(); for (Map.Entry<Long, Double> uv : userScores.entrySet()) { Long itemId = uv.getKey(); double userScore = uv.getValue(); Map<Long, Double> similarItems = similarityMatrix.getOrDefault(itemId, Collections.emptyMap()); for (Map.Entry<Long, Double> si : similarItems.entrySet()) { Long simItemId = si.getKey(); if (userScores.containsKey(simItemId)) { continue; } double sim = si.getValue(); recommendScores.put(simItemId, recommendScores.getOrDefault(simItemId, 0.0) + sim * userScore); simTotal.put(simItemId, simTotal.getOrDefault(simItemId, 0.0) + sim); } } return recommendScores.entrySet().stream() .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), e.getValue() / simTotal.getOrDefault(e.getKey(), 1.0))) .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .limit(topN) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> oldVal, LinkedHashMap::new )); } }

相似度矩阵不需要每次推荐实时计算。推荐服务每天凌晨跑一次离线任务,读取近三个月的用户行为数据,计算所有非遗项目之间的相似度并写入Redis。为了控制内存和计算量,我们只保留了每个项目相似度最高的前50个项目,因为再往后相似度已经非常低,保留对推荐结果几乎没有影响,还能大幅减少计算时间。

3.3 冷启动怎么办:HanLP分词构建内容画像

冷启动是协同过滤最常见的难题。新用户没有行为数据,新上架的非遗项目也没有人看过。这时候我们补了一套基于内容的推荐。

做法:维护nonheritage的内容标签表。标签来源有三块:

  • 管理员上传项目时手动打标签,比如“刺绣”“戏曲”“江南”
  • 对项目简介和传承人故事做HanLP分词,提取名词和关键词作为自动标签
  • 从项目所属类别、地区、级别等结构化字段映射出基础标签
List<Term> terms = HanLP.segment(heritageInfo.getIntro()); for (Term term : terms) { Nature nature = term.nature; if (nature.toString().startsWith("n") && term.word.length() > 1) { tagService.addTag(heritageId, term.word); } }

自动标签需要做过滤,去掉“我们”“这个”“发展”这类无意义词。我们维护了一个停用词表,并只保留词性为名词的词。新用户注册或者新项目入库后,推荐服务先基于内容标签计算“用户当前感兴趣的标签”和“项目标签的匹配度”,给出一个初始推荐列表。等用户积累了至少10条有效行为后,再切到协同过滤主模型。

内容推荐看起来简单,但效果比预想好。尤其是非遗平台,用户浏览一个“苏绣”后,大概率对“云锦”“缂丝”这类同属传统技艺的项目感兴趣,因为标签重合度高。

3.4 推荐接口的缓存和降级设计

推荐接口不能每次都算。在线接口从Redis缓存取结果,缓存key设计成recommend:user:{userId}:top20,缓存时间两小时。离线任务更新完相似度后,主动把受影响用户的缓存删掉,保证老用户第二天能看到新结果。

如果协同过滤结果为空,或者推荐服务挂了,前端会降级到热门项目接口。热门榜来自统计服务,按近七天的用户点击量排序。这个降级逻辑必须做,因为推荐位是首页流量入口,接口不能因为算法模块出了问题就整个挂掉。

4. 可视化大屏:非遗地图、热度榜单和用户画像怎么呈现

4.1 数据从哪来

可视化大屏不是前端随便画个图那么容易,后端统计服务才是核心。我们的统计服务定时从MySQL行为表汇聚数据,用Redis做实时计数,最后通过接口输出三类数据:

  • 全国非遗项目分布地图数据:按省统计项目数量、级别分布
  • 热门TOP榜:按观看、收藏、点赞行为加权排序
  • 用户画像:年龄、活跃时段、感兴趣类别Top10

查询接口还要支持时间范围筛选,方便运营看周报。大屏展示的是只读聚合数据,我们额外做了本地缓存,避免每次刷新都打爆数据库。统计服务所有接口都放在内网,通过网关暴露给前端,前端大屏只读。

4.2 大屏模块怎么设计

大屏前端用的是Vue3 + ECharts。整体布局参考了数据可视化大屏常见做法:顶部标题栏,中间是主视觉全国地图,左侧放推荐核心数据和用户行为趋势折线图,右侧放热门非遗榜单和类别占比饼图。

地图部分用的ECharts geo组件,地图数据用GeoJSON。这里有个实际细节:中国省级GeoJSON文件比较大,打包时要注意体积,建议从CDN异步加载,或者单独抽成静态资源,不要打进Vue的bundle里。我们第一次直接把地图GeoJSON作为js模块导入,首屏加载直接多了1.2MB,优化后改成静态文件异步加载,首屏速度提升明显。

用户画像模块我们做了词云图,标签数据来自推荐服务的标签表。词云能直观看到当前平台用户最关注的非遗方向,运营也可以根据词云调整内容采购方向。热门非遗榜则用柱状图滚动展示,支持点击跳转到对应项目详情页。

4.3 M3U8与视频播放

非遗项目中有大量视频资源,像传统戏剧、传统技艺的工艺流程都要靠视频展示。最开始我们是直接上传MP4,前端用video标签播放。但问题来了:MP4格式对网络要求高,大视频拖动进度条要加载很久。后来文件服务接入了MinIO + FFmpeg转码,把上传的视频统一转成M3U8切片格式。

Vue前端播放M3U8有两种方式:原生video通过hls.js播放,或者用video.js配置hls插件。我们最终选了hls.js,体积更小,可控性更强。

import Hls from 'hls.js' export function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(url) hls.attachMedia(videoElement) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoElement.play() }) } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { videoElement.src = url videoElement.addEventListener('loadedmetadata', () => { videoElement.play() }) } }

注意HTTPS环境下才能正常播放M3U8,生产环境我们部署了HTTPS证书,这才没有出现播放器能加载却一直黑屏的问题。转码耗时问题也要提前规划:FFmpeg转码很吃CPU,所以上传后的视频先进待转码队列,由单独的服务异步处理,转码完成后把M3U8地址写回数据库。

5. 联调与部署期的地狱级踩坑:从Nacos配置到Vue打包

5.1 Nacos配置中心动了,服务却还是旧配置

微服务配置集中到Nacos后,最典型的问题是:改了配置,服务不生效。原因大多是配置没有自动刷新。我们在bootstrap.yml中引入了spring-cloud-starter-alibaba-nacos-config,但忘了加@RefreshScope注解,导致改了Nacos里的配置,运行中的服务拿不到新值。后来给配置实体类加上@RefreshScope,配置变更才生效。

还有一个更隐蔽的坑:Nacos从2.x版本开始,默认不是完全兼容旧版客户端。我们有个服务用旧版spring-cloud-alibaba依赖连接Nacos,日志里一直报“Request nacos server failed”,服务启动不了。后来统一升级了spring-cloud-alibaba版本,问题才解决。团队里如果有多个人各自新建服务,一定要注意依赖版本统一,最好在父POM里做版本管理。

5.2 Gateway路由转发404和Feign超时

Gateway路由配置看似简单,但容易拼错路径。我们的/api/推荐服务前缀曾经写错,导致前端调推荐接口一直404。排查方法很简单:看Gateway日志,确认实际转发路径,再对比目标服务的Controller注解。网关里如果同时加了StripPrefix配置,路径处理会很绕,建议配置后写一个最小单元测试,直接发Sprint Boot测试请求。

Feign调用另一个容易踩的坑是超时时间太短。我们默认Ribbon超时是一秒,结果推荐服务内部要批量查MySQL和Redis,一秒经常不够。线上表现就是前端偶尔报“系统繁忙”,点几次又好了。这个不是网络抖动,而是Feign超时。我们把连接超时设为2秒,读取超时设为5秒,配合Sentinel熔断策略才稳定。

# application.yml feign: client: config: default: connectTimeout: 2000 readTimeout: 5000

Sentinel熔断参数也不是越大越好。我们一开始设置错误比例阈值为50%,结果服务繁忙时熔断太晚,拖垮了下游。后来改成20%,并且设置了最小请求数5,做到快速失败。

5.3 Vue打包之后接口地址失效问题

前后端分离部署,Vue打包后放在Nginx里,接口地址写的是 localhost:8080,测试环境没问题,打包到生产后所有请求全部走本地8080,自然找不到后端。这个问题主要源于环境变量没有区分。我们用.env.development和.env.production区分接口地址,生产环境构建时接口前缀要写服务器域名的网关地址。

还有一个容易被忽视的Nginx配置:Vue Router用了history模式后,刷新二级路由页面会404。需要在Nginx里做try_files重写:

location / { root /var/www/html; index index.html; try_files $uri $uri/ /index.html; }

如果不加这段,用户刷新大屏页面,直接白屏。

5.4 MinIO的对象存储权限和访问域名

MinIO部署后,一开始文件服务上传成功,但前端拿到的URL访问不了。因为MinIO默认桶权限是私有,访问需要预签名URL。我们给图片和可公开访问的视频单独设置了public-read权限,并配置了独立的访问域名,让MinIO和文件服务分离。前端上传视频这个场景,建议前端直传MinIO,避免经过后端转一道,否则大文件会把Java服务内存撑爆。我们是让前端先向后端申请一个临时上传凭证,拿到凭证后直接传到MinIO,上传完成后后端再收到回调,更新数据库记录。这一套流程稳定后,上传大视频就再也没出现OOM。

5.5 JVM参数和数据库连接池

部署阶段,我们自己统计过,推荐服务和统计服务的内存和CPU要求明显高于普通业务服务。推荐服务离线任务执行时,年轻代对象大量增加,如果不调大堆内存,频繁Full GC会拖垮在线接口。我们把推荐服务的JVM堆设成2GB,离线任务执行时用独立的线程池并限制并发数,避免和在线请求抢资源。

数据库连接池也踩过坑。多个微服务共用一个MySQL实例时,每个服务默认的HikariCP连接池大小是10,这不算大,但服务多了也容易把数据库连接数打爆。我们统一把最大连接数调整为20,并且只在真正高并发的服务上开事务。统计服务查询全部走只读副本,主库只承担业务写入。

6. 项目收尾后的几点实在建议

6.1 先跑通最小闭环,再补微服务和可视化

如果时间倒流,我可能会在第一版先把SPA单体应用做出来,验证推荐逻辑是不是合理。但实际项目既然定了微服务,也建议先拆3个核心服务跑通:非遗项目服务、推荐服务、网关服务。用户权限、统计、文件这些可以后续逐步补。微服务的复杂度和单体完全不是一个数量级,团队如果没有足够的运维能力,先把Nacos、Gateway、监控这些基础设施搭好,否则开发期会因为环境问题浪费大量时间。

6.2 协同过滤的收益要提前对齐预期

非遗平台的推荐场景和电商不一样,用户总数少、行为稀疏,协同过滤的效果不会像淘宝那样立刻带来转化率提升。我们在做项目汇报的时候,强调了推荐系统的“发现性”价值:让非遗爱好者能发现同一地区或同一类别的冷门项目。比如喜欢“皮影戏”的用户,可能很少听说“唐山皮影”和“环县道情皮影”,推荐系统能把这些关联项目推到用户面前。这才是非遗推荐相比热门榜真正的价值。

6.3 后续可以扩展的方向

这个项目的推荐服务目前还比较基础,后续可以加上定时更新相似度矩阵时的增量计算,避免每次全量跑。数据量上来之后,也可以引入图数据库来构建非遗项目的知识图谱,把传承人、工艺流派、代表作品都纳入推荐链路。可视化大屏后续可以加入实时视频热度地图,结合M3U8播放器的统计数据,动态展示哪个省的哪个非遗视频正在被观看。这套架构留出的扩展空间足够大,不至于做到一半又要推翻重来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询