P12 | 标签体系:灵活的多维标签设计与前端联动
💰 付费文章 | 第二阶段:后端开发
标签体系的价值
标签是连接用户偏好和内容推荐的核心桥梁:
用户偏好标签:["自然风光", "亲子游玩", "经济型"]
↕ 标签匹配
景点内容标签:["自然风光", "亲子友好", "免费入园"]
↓
推荐结果:匹配度排序
好的标签体系设计能同时支持:
- 景点内容打标签
- 用户偏好记录
- 精准搜索过滤
- 智能推荐排序
数据库设计
-- 标签分类
CREATE TABLE `PLAT_TAG_CATEGORY` (
`COMM_ID` VARCHAR(32) NOT NULL PRIMARY KEY,
`CATEGORY_NAME` VARCHAR(64) NOT NULL COMMENT '分类名',
`SORT` INT DEFAULT 0 COMMENT '排序',
`FLAG` INT DEFAULT 1
) COMMENT '标签分类';
-- 标签定义
CREATE TABLE `PLAT_TAG_DEFINITION` (
`COMM_ID` VARCHAR(32) NOT NULL PRIMARY KEY,
`CATEGORY_ID` VARCHAR(32) NOT NULL COMMENT '所属分类ID',
`TAG_ID` VARCHAR(64) NOT NULL COMMENT '标签英文标识(如 beach)',
`TAG_NAME` VARCHAR(64) NOT NULL COMMENT '标签中文名(如 海滩)',
`ICON` VARCHAR(128) COMMENT '图标URL',
`SORT` INT DEFAULT 0 COMMENT '排序',
`FLAG` INT DEFAULT 1,
UNIQUE KEY `uk_category_tagId` (`CATEGORY_ID`, `TAG_ID`)
) COMMENT '标签定义';
-- 用户偏好记录
CREATE TABLE `TOWN_USER_PREFERENCE` (
`COMM_ID` VARCHAR(32) NOT NULL PRIMARY KEY,
`USER_ID` VARCHAR(32) NOT NULL COMMENT '用户ID',
`TAG_ID` VARCHAR(64) NOT NULL COMMENT '偏好标签ID',
`ATTACH_DATA` VARCHAR(128) COMMENT '附加数据(如出行人数=2)',
`CREATE_TIME` DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_user_tag` (`USER_ID`, `TAG_ID`)
) COMMENT '用户偏好';
初始化标签数据
-- 7 大分类
INSERT INTO PLAT_TAG_CATEGORY (COMM_ID, CATEGORY_NAME, SORT) VALUES
('TC001', '城市特色', 100),
('TC002', '景点属性', 90),
('TC003', '体验特色', 80),
('TC004', '游客人群', 70),
('TC005', '消费特征', 60),
('TC006', '季节推荐', 50),
('TC007', '用户偏好', 40);
-- 城市特色标签
INSERT INTO PLAT_TAG_DEFINITION (COMM_ID, CATEGORY_ID, TAG_ID, TAG_NAME, SORT) VALUES
('TD001', 'TC001', 'coastal', '海滨城市', 1),
('TD002', 'TC001', 'mountain', '山岳城市', 2),
-- ... 更多标签
标签 API 接口
// 获取所有标签(按分类分组)
@PostMapping("/web/user/preference/options")
fun getPreferenceOptions(): Result<*> {
val categories = tagCategoryMapper.selectList(
LambdaQueryWrapper<PlatTagCategoryEntity>()
.eq(PlatTagCategoryEntity::flag, 1)
.orderByDesc(PlatTagCategoryEntity::sort)
)
val result = categories.map { category ->
val tags = tagDefinitionMapper.selectList(
LambdaQueryWrapper<PlatTagDefinitionEntity>()
.eq(PlatTagDefinitionEntity::categoryId, category.commId)
.eq(PlatTagDefinitionEntity::flag, 1)
.orderByAsc(PlatTagDefinitionEntity::sort)
)
mapOf(
"categoryId" to category.commId,
"categoryName" to category.categoryName,
"tags" to tags.map { tag ->
mapOf(
"tagId" to tag.tagId,
"tagName" to tag.tagName,
"icon" to (tag.icon ?: "")
)
}
)
}
return Result.ok(result)
}
// 保存用户偏好
@PostMapping("/web/user/preference/save")
fun savePreference(@RequestBody params: Map<String, Any>): Result<*> {
val currentUser = AuthInterceptor.currentUser.get() ?: return Result.unauthorized()
@Suppress("UNCHECKED_CAST")
val preferences = params["preferences"] as? List<Map<String, Any>> ?: emptyList()
// 先删除旧偏好
preferenceMapper.delete(
LambdaQueryWrapper<TownUserPreferenceEntity>()
.eq(TownUserPreferenceEntity::userId, currentUser.userId)
)
// 批量保存新偏好
preferences.forEach { pref ->
val entity = TownUserPreferenceEntity().apply {
commId = IdGenerator.generate("UPF")
userId = currentUser.userId
tagId = pref["tagId"] as? String ?: ""
attachData = pref["attachData"] as? String
createTime = Date()
}
preferenceMapper.insert(entity)
}
return Result.ok(null, "保存成功")
}
基于标签的推荐
// 根据用户偏好标签推荐景点
override fun recommendByPreference(userId: String, limit: Int): List<Map<String, Any?>> {
// 1. 获取用户偏好标签
val userTags = preferenceMapper.selectList(
LambdaQueryWrapper<TownUserPreferenceEntity>()
.eq(TownUserPreferenceEntity::userId, userId)
).map { it.tagId }.toSet()
if (userTags.isEmpty()) {
// 无偏好,返回热门景点
return getHotAttractions(limit)
}
// 2. 查所有有效景点,计算匹配度
val attractions = attractionMapper.selectList(
LambdaQueryWrapper<TownAttractionEntity>()
.eq(TownAttractionEntity::flag, 1)
)
return attractions
.map { attraction ->
val attractionTags = parseJsonList(attraction.tags).toSet()
val matchCount = userTags.intersect(attractionTags).size
Triple(attraction, matchCount, attractionTags)
}
.filter { (_, matchCount, _) -> matchCount > 0 }
.sortedByDescending { (_, matchCount, _) -> matchCount }
.take(limit)
.map { (attraction, _, _) -> assembleAttractionCard(attraction) }
}