P12 | 标签体系:灵活的多维标签设计与前端联动

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) }
}

下一篇

P13 → 异步任务:后台长时间操作的最佳实践

相关推荐
计算机魔术师24 分钟前
烧掉2780亿美元还不够?OpenAI的资本豪赌让人头皮发麻
前端
IT_陈寒1 小时前
Java线程池用错参数,我的服务居然悄悄崩溃了
前端·人工智能·后端
卡布鲁1 小时前
React useMemo 与 useCallback 完全指南:原理、场景与避坑
前端·react.js
碳基修炼2 小时前
排查记:本地 devServer 是 http,浏览器却把 302 重定向升级成了 https
前端·http
步行cgn2 小时前
Spring p 命名空间注入详解
java·前端·spring
何何____2 小时前
Vue 生命周期详解
前端·javascript
江米小枣tonylua2 小时前
TypeScript 全面の拥抱 !Prisma 8 + Electron 升级实战
前端
闪耀之光M782 小时前
npm命令解析
前端
咸鱼老弟3 小时前
AI Agent 的"自主性悖论"——为什么给它的自由度越大,越要配一套更硬的护栏
前端·人工智能
默_笙3 小时前
💫 闭包是个背包:拆解小米前端面试题里的三道"闭包陷阱"
前端·javascript·面试