最近在做一个文创内容推荐平台的项目,用Spring Boot和Vue.js搭建了一套前后端分离的系统。这个项目的核心目标是解决文创产业中的两个痛点:一方面用户面对海量文创内容难以找到真正感兴趣的东西,另一方面优质文创作品又缺乏有效的曝光渠道。
我选择的技术栈是:
这个组合在性能和开发效率上达到了很好的平衡。Spring Boot的自动配置和起步依赖让后端服务搭建变得非常简单,而Vue的响应式特性和组件化开发则让前端交互体验更加流畅。
我们采用了经典的前后端分离架构,这种设计有几个明显优势:
后端API采用RESTful风格设计,所有接口都遵循统一的响应格式:
json复制{
"code": 200,
"message": "success",
"data": {...}
}
MySQL数据库设计了以下几张核心表:
用户表(user)
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`interests` varchar(255) DEFAULT NULL COMMENT '兴趣标签,逗号分隔',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
文创内容表(content)
sql复制CREATE TABLE `content` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`description` text,
`cover_image` varchar(255) DEFAULT NULL,
`content_type` tinyint DEFAULT NULL COMMENT '1-文创产品 2-数字内容 3-活动',
`tags` varchar(255) DEFAULT NULL COMMENT '标签,逗号分隔',
`creator_id` bigint DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_creator` (`creator_id`),
KEY `idx_type` (`content_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
用户行为表(user_behavior)
sql复制CREATE TABLE `user_behavior` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`content_id` bigint NOT NULL,
`behavior_type` tinyint NOT NULL COMMENT '1-浏览 2-收藏 3-购买 4-评分',
`behavior_value` varchar(255) DEFAULT NULL COMMENT '评分值或评价内容',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_content` (`user_id`,`content_id`),
KEY `idx_content` (`content_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
用户冷启动阶段,我们通过注册时选择的兴趣标签初始化用户画像。随着用户使用,系统会记录以下行为数据:
这些数据通过定时任务(Spring Scheduler)每天凌晨处理,更新用户画像:
java复制@Scheduled(cron = "0 0 3 * * ?")
public void updateUserProfiles() {
List<Long> userIds = userMapper.getAllActiveUserIds();
for(Long userId : userIds) {
UserProfile profile = buildUserProfile(userId);
redisTemplate.opsForValue().set("user:profile:"+userId,
JSON.toJSONString(profile));
}
}
private UserProfile buildUserProfile(Long userId) {
// 获取用户基础信息
User user = userMapper.selectById(userId);
// 获取用户近期行为
List<UserBehavior> behaviors = behaviorMapper
.selectRecentBehaviors(userId, 30);
// 计算标签权重
Map<String, Double> tagWeights = calculateTagWeights(behaviors);
// 构建画像对象
UserProfile profile = new UserProfile();
profile.setUserId(userId);
profile.setBaseTags(user.getInterests());
profile.setBehaviorTags(tagWeights);
profile.setUpdateTime(new Date());
return profile;
}
我们实现了三种推荐策略,根据场景灵活调用:
1. 基于内容的推荐
java复制public List<Content> recommendByContent(Long userId, int size) {
// 从Redis获取用户画像
String profileJson = redisTemplate.opsForValue()
.get("user:profile:"+userId);
UserProfile profile = JSON.parseObject(profileJson, UserProfile.class);
// 获取用户偏好标签(按权重排序)
List<String> preferredTags = profile.getSortedTags();
// 根据标签匹配内容
return contentMapper.selectByTags(preferredTags, size);
}
2. 协同过滤推荐
java复制public List<Content> recommendByCF(Long userId, int size) {
// 找到相似用户
List<Long> similarUsers = findSimilarUsers(userId);
// 获取相似用户喜欢的内容
Set<Long> viewedContents = behaviorMapper
.selectViewedContentsByUser(userId);
return contentMapper.selectPopularInUserGroup(
similarUsers, viewedContents, size);
}
3. 热门推荐
java复制public List<Content> recommendHot(int size) {
// 综合浏览量、收藏量、评分计算热度
return contentMapper.selectHotContents(size);
}
在实际调用时,我们会根据用户活跃度混合使用这些策略:
java复制public List<Content> recommendForUser(Long userId, int size) {
// 新用户或低活跃用户使用热门+内容推荐
if(isNewUser(userId) || isLowActiveUser(userId)) {
List<Content> hot = recommendHot(size/2);
List<Content> contentBased = recommendByContent(userId, size/2);
return mergeRecommendations(hot, contentBased);
}
// 老用户使用协同过滤+内容推荐
List<Content> cf = recommendByCF(userId, size/2);
List<Content> contentBased = recommendByContent(userId, size/2);
return mergeRecommendations(cf, contentBased);
}
为了减轻数据库压力,我们采用了多级缓存:
java复制@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}
java复制public List<Content> selectAfterId(Long lastId, int size) {
return contentMapper.selectAfterId(lastId, size);
}
为避免实时计算带来的延迟,我们采用了以下策略:
问题:新用户没有行为数据,难以进行个性化推荐
解决方案:
问题:用户-内容交互矩阵非常稀疏,影响协同过滤效果
解决方案:
问题:用户增长后推荐接口响应变慢
优化措施:
Vue前端的主要技术选型:
首页推荐组件关键代码:
vue复制<template>
<div class="recommend-container">
<h3>为你推荐</h3>
<div v-if="loading" class="loading">
<el-skeleton :rows="3" animated />
</div>
<el-scrollbar v-else>
<div class="content-list">
<content-card
v-for="item in recommendList"
:key="item.id"
:content="item"
@click="handleClick(item.id)"
/>
</div>
</el-scrollbar>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getRecommendations } from '@/api/recommend'
import ContentCard from '@/components/ContentCard.vue'
const recommendList = ref([])
const loading = ref(true)
onMounted(async () => {
try {
const res = await getRecommendations(10)
recommendList.value = res.data
} catch (error) {
console.error('获取推荐失败', error)
} finally {
loading.value = false
}
})
const handleClick = (contentId) => {
// 记录点击行为
recordBehavior(contentId, 'click')
// 跳转到详情页
router.push(`/content/${contentId}`)
}
</script>
为了不干扰用户体验,我们采用了无感知的行为采集方式:
javascript复制// 内容曝光采集
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
const contentId = entry.target.dataset.contentId
recordImpression(contentId)
}
})
}, {threshold: 0.5})
onMounted(() => {
document.querySelectorAll('.content-item').forEach(el => {
observer.observe(el)
})
})
// 停留时间采集
let startTime = 0
const recordStayTime = () => {
if(startTime > 0) {
const stayTime = Date.now() - startTime
if(stayTime > 3000) { // 只记录超过3秒的停留
recordBehavior(contentId, 'stay', stayTime)
}
}
startTime = Date.now()
}
onBeforeUnmount(() => {
recordStayTime()
})
我们使用Docker Compose进行容器化部署,docker-compose.yml主要配置:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: cultural_db
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries: 5
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 10s
retries: 5
backend:
build: ./backend
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/cultural_db
SPRING_REDIS_HOST: redis
ports:
- "8080:8080"
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql_data:
redis_data:
部署流程:
npm run buildmvn clean packagedocker-compose up -d这个文创推荐平台项目让我对推荐系统有了更深入的理解。几点关键经验:
数据质量决定推荐效果:初期过于关注算法复杂度,后来发现用户行为数据的质量才是关键。增加了数据清洗和异常值处理模块后,推荐准确率提升了30%
混合策略更稳健:纯算法推荐在特定场景下容易陷入信息茧房,适当加入人工运营规则(如新品扶持、多样性控制)能显著提升用户体验
性能与效果的平衡:在资源有限的情况下,需要在算法精度和响应速度之间找到平衡点。我们最终采用了离线计算+实时修正的混合方案
AB测试必不可少:通过AB测试对比不同推荐策略,发现对老用户使用协同过滤+内容混合推荐,CTR比单一策略高出15-20%
这个项目还有很多优化空间,比如引入深度学习模型、增加社交关系维度等。但目前的版本已经验证了核心业务流程,为后续迭代打下了良好基础。