P11 | 收藏与行程:用户行为类接口的设计模式

P11 | 收藏与行程:用户行为类接口的设计模式

💰 付费文章 | 第二阶段:后端开发


用户行为类接口的特点

「收藏」「点赞」「关注」「加入购物车」------这类接口有共同特征:

  1. 幂等性:重复操作结果一致(收藏已收藏的 = 无效果)
  2. 用户隔离:每个用户只能看到自己的数据
  3. 关联查询:返回数据时需要关联业务实体(景点名称、封面图等)
  4. 计数更新:有时需要更新关联实体的计数字段(收藏数、点赞数)

收藏功能实现

数据库表

复制代码
CREATE TABLE `TOWN_FAVORITE` (
    `COMM_ID`       VARCHAR(32) NOT NULL PRIMARY KEY,
    `USER_ID`       VARCHAR(32) NOT NULL   COMMENT '用户ID',
    `ATTRACTION_ID` VARCHAR(32) NOT NULL   COMMENT '景点ID',
    `FLAG`          INT DEFAULT 1          COMMENT '1有效 0取消',
    `CREATE_TIME`   DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_user_attraction` (`USER_ID`, `ATTRACTION_ID`)
) COMMENT '用户收藏';

Service 实现

复制代码
@Service
class FavoriteServiceImpl(
    private val favoriteMapper: TownFavoriteMapper,
    private val attractionMapper: TownAttractionMapper
) : FavoriteService {

    /**
     * 切换收藏状态(幂等)
     */
    override fun toggle(userId: String, attractionId: String): Map<String, Any> {
        // 查询是否已有记录
        val existing = favoriteMapper.selectOne(
            LambdaQueryWrapper<TownFavoriteEntity>()
                .eq(TownFavoriteEntity::userId, userId)
                .eq(TownFavoriteEntity::attractionId, attractionId)
        )
        
        return if (existing == null) {
            // 首次收藏
            val entity = TownFavoriteEntity().apply {
                commId = IdGenerator.generate("FAV")
                this.userId = userId
                this.attractionId = attractionId
                flag = 1
            }
            favoriteMapper.insert(entity)
            mapOf("favorited" to true, "msg" to "已收藏")
        } else if (existing.flag == 1) {
            // 已收藏 → 取消
            existing.flag = 0
            favoriteMapper.updateById(existing)
            mapOf("favorited" to false, "msg" to "已取消收藏")
        } else {
            // 之前取消过 → 重新收藏
            existing.flag = 1
            favoriteMapper.updateById(existing)
            mapOf("favorited" to true, "msg" to "已收藏")
        }
    }

    /**
     * 查询收藏列表(关联景点数据)
     */
    override fun getPageList(userId: String, page: Int, pageSize: Int): PageResult<Map<String, Any?>> {
        val favorites = favoriteMapper.selectPage(
            Page(page.toLong(), pageSize.toLong()),
            LambdaQueryWrapper<TownFavoriteEntity>()
                .eq(TownFavoriteEntity::userId, userId)
                .eq(TownFavoriteEntity::flag, 1)
                .orderByDesc(TownFavoriteEntity::createTime)
        )
        
        // 批量查询景点信息(避免 N+1 查询)
        val attractionIds = favorites.records.map { it.attractionId }
        val attractionMap = if (attractionIds.isNotEmpty()) {
            attractionMapper.selectBatchIds(attractionIds).associateBy { it.commId }
        } else emptyMap()
        
        val result = favorites.records.mapNotNull { fav ->
            val attraction = attractionMap[fav.attractionId] ?: return@mapNotNull null
            mapOf(
                "favoriteId" to fav.commId,
                "attractionId" to attraction.commId,
                "attractionName" to attraction.attName,
                "coverImage" to attraction.coverImage,
                "tags" to parseJsonList(attraction.tags),
                "description" to (attraction.highlight ?: ""),
                "createTime" to fav.createTime
            )
        }
        
        return PageResult(result, favorites.total, page.toLong(), pageSize.toLong(), favorites.pages)
    }
}

行程功能实现

数据库表

复制代码
-- 行程主表
CREATE TABLE `TOWN_ITINERARY` (
    `COMM_ID`         VARCHAR(32) NOT NULL PRIMARY KEY,
    `USER_ID`         VARCHAR(32) NOT NULL  COMMENT '创建者',
    `CITY_ID`         VARCHAR(32)           COMMENT '城市ID',
    `TITLE`           VARCHAR(128) NOT NULL COMMENT '行程名称',
    `START_DATE`      DATE                  COMMENT '开始日期',
    `END_DATE`        DATE                  COMMENT '结束日期',
    `IS_PUBLIC`       INT DEFAULT 0         COMMENT '是否公开 0否 1是',
    `ATTRACTION_COUNT` INT DEFAULT 0        COMMENT '景点数量(冗余)',
    `FLAG`            INT DEFAULT 1,
    `CREATE_TIME`     DATETIME DEFAULT CURRENT_TIMESTAMP,
    `UPDATE_TIME`     DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '行程表';

-- 行程-景点关联表
CREATE TABLE `TOWN_ITINERARY_ITEM` (
    `COMM_ID`       VARCHAR(32) NOT NULL PRIMARY KEY,
    `ITINERARY_ID`  VARCHAR(32) NOT NULL COMMENT '行程ID',
    `ATTRACTION_ID` VARCHAR(32) NOT NULL COMMENT '景点ID',
    `DAY_NUM`       INT DEFAULT 1        COMMENT '第几天',
    `SORT`          INT DEFAULT 0        COMMENT '当天顺序',
    `REMARK`        VARCHAR(256)         COMMENT '备注',
    `FLAG`          INT DEFAULT 1,
    `CREATE_TIME`   DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '行程景点明细';

行程保存(创建/更新)

复制代码
@Transactional
override fun saveItinerary(userId: String, params: Map<String, Any>): String {
    val itineraryId = params["id"] as? String
    
    // 1. 保存行程主体
    val itinerary = if (itineraryId != null) {
        itineraryMapper.selectById(itineraryId)?.also { entity ->
            // 验证是当前用户的行程
            require(entity.userId == userId) { "无权修改他人行程" }
        } ?: throw BusinessException("行程不存在")
    } else {
        TownItineraryEntity().apply { commId = IdGenerator.generate("ITI"); this.userId = userId; flag = 1 }
    }
    
    itinerary.title = params["title"] as? String ?: "我的行程"
    itinerary.cityId = params["cityId"] as? String
    itinerary.startDate = parseDate(params["startDate"] as? String)
    itinerary.endDate = parseDate(params["endDate"] as? String)
    itinerary.isPublic = (params["isPublic"] as? Boolean)?.let { if (it) 1 else 0 } ?: 0
    
    itineraryMapper.saveOrUpdate(itinerary)
    
    // 2. 保存景点明细(先删后插)
    @Suppress("UNCHECKED_CAST")
    val items = params["items"] as? List<Map<String, Any>> ?: emptyList()
    
    itineraryItemMapper.delete(
        LambdaQueryWrapper<TownItineraryItemEntity>()
            .eq(TownItineraryItemEntity::itineraryId, itinerary.commId)
    )
    
    items.forEachIndexed { index, item ->
        val itemEntity = TownItineraryItemEntity().apply {
            commId = IdGenerator.generate("ITM")
            itineraryId = itinerary.commId
            attractionId = item["attractionId"] as? String
            dayNum = item["dayNum"] as? Int ?: 1
            sort = index
            remark = item["remark"] as? String
            flag = 1
        }
        itineraryItemMapper.insert(itemEntity)
    }
    
    // 3. 更新景点数量
    itinerary.attractionCount = items.size
    itineraryMapper.updateById(itinerary)
    
    return itinerary.commId!!
}

下一篇

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

相关推荐
weixin_408717771 天前
mysql权限表查询性能如何优化_MySQL系统权限缓存原理
jvm·数据库·python
吕源林1 天前
怎么优化MongoDB的软删除设计_布尔标记与删除时间戳
jvm·数据库·python
吕源林1 天前
如何解决SQL存储过程连接泄露_确保在异常后关闭连接
jvm·数据库·python
Gofarlic_OMS1 天前
应对MathWorks合规审查的专项准备工作
大数据·服务器·网络·数据库·人工智能
七夜zippoe1 天前
DolphinDB SQL查询:从基础到进阶
数据库·sql·进阶·聚合·dolphindb
有想法的py工程师1 天前
PostgreSQL 深入heap_update() 与 HOT 机制(附源码级解析)
数据库·postgresql
断眉的派大星1 天前
工厂模式(Factory Pattern)完整详解
python·设计模式
qq_342295821 天前
如何为容器内多个列表实现统一滚动条.txt
jvm·数据库·python
qq_206901391 天前
CSS如何引入自适应图标_利用svg外链配合css控制颜色
jvm·数据库·python