主页数据显示讲解

一、整体架构图

首页列表查询架构-8294a913
  • 第一层(高速查询层):请求首先打入 Elasticsearch
    • 利用倒排索引解决多条件(地区+分类)查询问题。
    • 数据为“宽表”结构,包含展示所需的所有字段(时间、价格范围),无需二次查询。
  • 第二层(兜底保障层):如果 ES 挂了或无数据,回退到 Database (MySQL)
    • 核心痛点:数据库按 ID 分片,按地区/分类查询会导致“全路由扫描(读扩散)”。
    • 优化手段:MyBatis 动态拼接 UNION ALL + Java 内存组装,最大限度减少 DB 交互次数。

二、核心问题分析

1. 为什么常规方案不行?

为什么常规方案不行-3ba90c7d
  • 分库分表限制d_program 表按主键 ID 分片。查询条件却是 areaId(地区)和 parentCategoryId(父分类)。
  • 读扩散(Read Diffusion):由于查询条件不是分片键,数据库中间件必须向所有分片发送 SQL 请求,然后汇总结果。随着分片增加,性能线性下降。
  • 路由表失效
    • 即便是建立 area_id -> program_idcategory_id -> program_id 的路由表。
    • 问题:两个路由表是独立的,无法在数据库层面高效求“交集”。在应用层把两个大集合拉出来求交集,内存和网络开销巨大。

2. Elasticsearch 解决方案

Elasticsearch 学习笔记

_Elasticsearch_解决方案-00b60456
  • 倒排索引:ES 天生适合多维度筛选。它将 areaIdcategoryId 建立索引,查询时直接取交集,时间复杂度极低。
  • 宽表设计:为了避免“查出 ID 后再去 DB 查详情”,ES 中的 Document 包含了 minPriceshowTime 等聚合数据(在数据初始化/写入时预计算好)。
  • 高可用策略:ES 作为主查询源,MySQL 作为兜底,保证系统健壮性。

三、代码执行流程

1. 主入口逻辑

入参实体

使用了jakarta.validation的注解来做参数验证,区域id不能为空,父节目类型id集合不能为空并且长度最大为4

@Data
@ApiModel(value="ProgramListDto", description ="主页节目列表")
public class ProgramListDto {
    
    @ApiModelProperty(name ="areaId", dataType ="Long", value ="所在区域id")
    private Long areaId;
    
    @ApiModelProperty(name ="parentProgramCategoryIds", dataType ="Long[]", value ="父节目类型id集合",required = true)
    @NotNull
    @Size(max = 4)
    private List<Long> parentProgramCategoryIds;
}

控制层 com.damai.controller.ProgramController#selectHomeList

@ApiOperation(value = "查询主页列表")
@PostMapping(value = "/home/list")
public ApiResponse<List<ProgramHomeVo>> selectHomeList(@Valid @RequestBody ProgramListDto programPageListDto) {
    return ApiResponse.ok(programService.selectHomeList(programPageListDto));
}

实现:

核心策略:ES 优先,DB 兜底

public List<ProgramHomeVo> selectHomeList(ProgramListDto programPageListDto) {

    // ==================== 策略1:优先从ES查询 (高速路) ====================
    // ES 中存放了组装好的宽表数据,查询速度快,无读扩散问题
    List<ProgramHomeVo> programHomeVoList = programEs.selectHomeList(programPageListDto);

    if (CollectionUtil.isNotEmpty(programHomeVoList)) {
        return programHomeVoList;  // 命中缓存/索引,直接返回
    }

    // ==================== 策略2:ES没数据,数据库兜底 (应急车道) ====================
    // 当 ES 服务不可用或数据同步延迟时,执行复杂的 DB 查询逻辑
    return dbSelectHomeList(programPageListDto);
}

2. ES 查询详解

核心逻辑:循环分类查询 + 聚合结果

public List<ProgramHomeVo> selectHomeList(ProgramListDto programPageListDto) {
    List<ProgramHomeVo> programHomeVoList = new ArrayList<>();

    try {
        // ============ 循环每个父节目类型(最多4个)============
        // 业务需求是每个楼层(分类)显示前7个,ES一次Query很难实现“分组取TopN”
        // 所以在应用层循环 4 次,对 ES 压力很小
        for (Long parentProgramCategoryId : programPageListDto.getParentProgramCategoryIds()) {

            List<EsDataQueryDto> programEsQueryDto = new ArrayList<>();

            // ---------- 构建查询条件 ----------
            if (Objects.nonNull(programPageListDto.getAreaId())) {
                // 情况A:查特定地区(如北京)
                EsDataQueryDto areaIdQueryDto = new EsDataQueryDto();
                areaIdQueryDto.setParamName(ProgramDocumentParamName.AREA_ID);
                areaIdQueryDto.setParamValue(programPageListDto.getAreaId());
                programEsQueryDto.add(areaIdQueryDto);
            } else {
                // 情况B:查全国 → 只查“主要节目”(Prime),避免同一巡演显示多个城市
                EsDataQueryDto primeQueryDto = new EsDataQueryDto();
                primeQueryDto.setParamName(ProgramDocumentParamName.PRIME);
                primeQueryDto.setParamValue(BusinessStatus.YES.getCode());
                programEsQueryDto.add(primeQueryDto);
            }

            // 父节目类型条件
            EsDataQueryDto parentCategoryDto = new EsDataQueryDto();
            parentCategoryDto.setParamName(ProgramDocumentParamName.PARENT_PROGRAM_CATEGORY_ID);
            parentCategoryDto.setParamValue(parentProgramCategoryId);
            programEsQueryDto.add(parentCategoryDto);

            // ---------- 执行查询(每个类型取前7条)----------
            // 这里的 queryPage 底层调用 ES 的 Search API
            PageInfo<ProgramListVo> pageInfo = businessEsHandle.queryPage(
                    indexName, indexType, programEsQueryDto,
                    1,    // 第1页
                    7,    // 取7条
                    ProgramListVo.class
            );

            // ---------- 组装结果 ----------
            if (!pageInfo.getList().isEmpty()) {
                ProgramHomeVo programHomeVo = new ProgramHomeVo();
                // 设置楼层名称(如“演唱会”)
                programHomeVo.setCategoryName(pageInfo.getList().get(0).getParentProgramCategoryName());
                programHomeVo.setCategoryId(pageInfo.getList().get(0).getParentProgramCategoryId());
                programHomeVo.setProgramListVoList(pageInfo.getList());
                programHomeVoList.add(programHomeVo);
            }
        }
    } catch (Exception e) {
        // 关键点:ES查询异常不抛出,而是记录日志,让流程继续向下走 DB 兜底
        log.error("businessEsHandle.queryPage error", e);
    }
    return programHomeVoList;
}
ES 查询详解-c5c754ad

3. ES 中的数据结构

特点:空间换时间,宽表存储

{
  "_source": {
    // ========== 基本信息 (来自 d_program) ==========
    "id": 6,
    "title": "「万朵」—春日玫瑰2024新专辑巡演",
    "itemPicture": "https://xxx/pkYEDAI.webp",

    // ========== 地区信息 (来自 d_area 或 关联查询) ==========
    "areaId": 2,
    "areaName": "北京",

    // ========== 分类信息 (来自 d_program_category) ==========
    "programCategoryId": 1,
    "parentProgramCategoryName": "演唱会",

    // ========== 预计算字段 (核心优化) ==========
    // 原本分散在 d_program_show_time 和 d_ticket_category 表中
    // 在数据同步到 ES 前,已经计算好最小值、最大值和首场时间
    "showTime": 1720182600000,
    "showWeekTime": "周五",
    "minPrice": 156,
    "maxPrice": 299,

    // ========== 业务标识 ==========
    "prime": 1  // 1=主节目(用于全国列表去重)
  }
}
ES_预组装数据的优势-081f76de

4. 数据库兜底查询详解

核心逻辑:批量查询 + 内存组装 (解决 N+1 问题)

数据库兜底查询详解-00885554
private List<ProgramHomeVo> dbSelectHomeList(ProgramListDto programPageListDto){
    List<ProgramHomeVo> programHomeVoList = new ArrayList<>();
    //根据父节目类型id来查询节目类型map,key:节目类型id,value:节目类型名
    Map<Long, String> programCategoryMap = selectProgramCategoryMap(programPageListDto.getParentProgramCategoryIds());
    //查询节目列表
    List<Program> programList = programMapper.selectHomeList(programPageListDto);
    //如果节目列表为空,那么直接返回空map
    if (CollectionUtil.isEmpty(programList)) {
        return programHomeVoList;
    }
    //节目id集合
    List<Long> programIdList = programList.stream().map(Program::getId).collect(Collectors.toList());
    //根据节目id集合查询节目演出时间集合
    LambdaQueryWrapper<ProgramShowTime> programShowTimeLambdaQueryWrapper = Wrappers.lambdaQuery(ProgramShowTime.class)
            .in(ProgramShowTime::getProgramId, programIdList);
    List<ProgramShowTime> programShowTimeList = programShowTimeMapper.selectList(programShowTimeLambdaQueryWrapper);
    //将节目演出集合根据节目id进行分组成map,key:节目id,value:节目演出时间集合
    Map<Long, List<ProgramShowTime>> programShowTimeMap = 
            programShowTimeList.stream().collect(Collectors.groupingBy(ProgramShowTime::getProgramId));
    //根据节目id统计出票档的最低价和最高价的集合map, key:节目id,value:票档
    Map<Long, TicketCategoryAggregate> ticketCategorieMap = selectTicketCategorieMap(programIdList);
    //将节目结合按照父节目类型id进行分组成map,key:父节目类型id,value:节目集合
    Map<Long, List<Program>> programMap = programList.stream()
            .collect(Collectors.groupingBy(Program::getParentProgramCategoryId));
    //循环此map
    for (Entry<Long, List<Program>> programEntry : programMap.entrySet()) {
        //父节目类型id
        Long key = programEntry.getKey();
        //节目集合
        List<Program> value = programEntry.getValue();
        List<ProgramListVo> programListVoList = new ArrayList<>();
        //循环节目集合
        for (Program program : value) {
            ProgramListVo programListVo = new ProgramListVo();
            BeanUtil.copyProperties(program,programListVo);
            //演出时间
            programListVo.setShowTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                    .filter(list -> !list.isEmpty())
                    .map(list -> list.get(0))
                    .map(ProgramShowTime::getShowTime)
                    .orElse(null));
            //演出时间(精确到天)
            programListVo.setShowDayTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                    .filter(list -> !list.isEmpty())
                    .map(list -> list.get(0))
                    .map(ProgramShowTime::getShowDayTime)
                    .orElse(null));
            //演出时间所在的星期
            programListVo.setShowWeekTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                    .filter(list -> !list.isEmpty())
                    .map(list -> list.get(0))
                    .map(ProgramShowTime::getShowWeekTime)
                    .orElse(null));
            //节目最高价
            programListVo.setMaxPrice(Optional.ofNullable(ticketCategorieMap.get(program.getId()))
                    .map(TicketCategoryAggregate::getMaxPrice).orElse(null));
            //节目最低价
            programListVo.setMinPrice(Optional.ofNullable(ticketCategorieMap.get(program.getId()))
                    .map(TicketCategoryAggregate::getMinPrice).orElse(null));
            programListVoList.add(programListVo);
        }
        ProgramHomeVo programHomeVo = new ProgramHomeVo();
        //节目类型名
        programHomeVo.setCategoryName(programCategoryMap.get(key));
        //节目类型id
        programHomeVo.setCategoryId(key);
        programHomeVo.setProgramListVoList(programListVoList);
        //节目列表
        programHomeVoList.add(programHomeVo);
    }
    return programHomeVoList;
}

从数据库中查询没有办法像从elasticsearch中直接查询的数据都是全的,programCategoryName、showTime、showDayTime、showWeekTime、minPrice、maxPrice这些仍然要从其他表中获取的,这些查询中都带有分片键,所以并不需要担心全路由的问题

查询节目类型名字

//根据父节目类型id来查询节目类型map,key:节目类型id,value:节目类型名
Map<Long, String> programCategoryMap = selectProgramCategoryMap(programPageListDto.getParentProgramCategoryIds());
/**
 * 根据节目类型id集合查询节目类型名字
 * @param programCategoryIdList 节目类型id集合
 * @return Map<Long, String> key:节目类型id value:节目类型名字
 * */
public Map<Long, String> selectProgramCategoryMap(Collection<Long> programCategoryIdList){
    LambdaQueryWrapper<ProgramCategory> pcLambdaQueryWrapper = Wrappers.lambdaQuery(ProgramCategory.class)
            .in(ProgramCategory::getId, programCategoryIdList);
    List<ProgramCategory> programCategoryList = programCategoryMapper.selectList(pcLambdaQueryWrapper);
    return programCategoryList
            .stream()
            .collect(Collectors.toMap(ProgramCategory::getId, ProgramCategory::getName, (v1, v2) -> v2));
}

根据传入的parentProgramCategoryIds父节目类型id集合,获取相关数据

查询节目列表

List<Program> programList = programMapper.selectHomeList(programPageListDto);

这个查询的要求是查询根据每个parentProgramCategoryId查询出节目列表,并且取前7条,要用sql实现的话,就要这么做,前端传入参数以此为例:

{
    "areaId":"2",
    "parentProgramCategoryIds":["1","2"]
}

形成的sql为

select
        dp.id,dp.area_id,dp.program_category_id,dp.parent_program_category_id,dp.title,dp.actor,
        dp.place,dp.item_picture
    from
        d_program dp
    where
        dp.program_status = 1
    and
        dp.area_id = 2
    and
        dp.parent_program_category_id = 1 limit 7
) as tmp
union all  
select * from (
    select
        dp.id,dp.area_id,dp.program_category_id,dp.parent_program_category_id,dp.title,dp.actor,
        dp.place,dp.item_picture
    from
        d_program dp
    where
        dp.program_status = 1
    and
        dp.area_id = 2
    and
        dp.parent_program_category_id = 2 limit 7
) as tmp

每个根据父节目id的查询都是一个个独立的sql,然后靠union all 来拼接起来,要想在代码中实现这种效果,就需要使用Mybatis来动态拼接sql,自定义写我们的sql查询逻辑

com.damai.mapper.ProgramMapper#selectHomeList

/**
 * 主页查询
 * @param programListDto 参数
 * @return 结果
 * */
List<Program> selectHomeList(@Param("programListDto")ProgramListDto programListDto);

对应的xml文件

<select id="selectHomeList" parameterType="com.damai.dto.ProgramListDto" resultType="com.damai.entity.Program">
    <if test = 'programListDto.parentProgramCategoryIds != null and programListDto.parentProgramCategoryIds.size>0'>
        <foreach collection='programListDto.parentProgramCategoryIds' item='parentProgramCategoryId' index='index' separator=' union all '>
            select * from (
                select
                    dp.id,dp.area_id,dp.program_category_id,dp.parent_program_category_id,dp.title,dp.actor,
                    dp.place,dp.item_picture
                from
                    d_program dp
                where
                    dp.program_status = 1
                <if test ='programListDto.areaId == null'>
                    and dp.prime = 1
                </if>
                <if test ='programListDto.areaId != null'>
                    and dp.area_id = #{programListDto.areaId,jdbcType=BIGINT}
                </if>
                and
                    dp.parent_program_category_id = #{parentProgramCategoryId,jdbcType=BIGINT} limit 7
            ) as tmp
        </foreach>
    </if>
</select>

使用了foreach循环标签,来循环parentProgramCategoryIds,并使用标签的separator属性,来将union all进行分隔,这样就能查询出节目列表了

接下来就是接着去别的表中查询需要的数据

查询节目演出时间

//节目id集合
List<Long> programIdList = programList.stream().map(Program::getId).collect(Collectors.toList());
//根据节目id集合查询节目演出时间集合
LambdaQueryWrapper<ProgramShowTime> programShowTimeLambdaQueryWrapper = Wrappers.lambdaQuery(ProgramShowTime.class)
        .in(ProgramShowTime::getProgramId, programIdList);
List<ProgramShowTime> programShowTimeList = programShowTimeMapper.selectList(programShowTimeLambdaQueryWrapper);
//将节目演出集合根据节目id进行分组成map,key:节目id,value:节目演出时间集合
Map<Long, List<ProgramShowTime>> programShowTimeMap = 
        programShowTimeList.stream().collect(Collectors.groupingBy(ProgramShowTime::getProgramId));

根据节目id统计出票档的最低价和最高价的集合

//根据节目id统计出票档的最低价和最高价的集合map, key:节目id,value:票档
Map<Long, TicketCategoryAggregate> ticketCategorieMap = selectTicketCategorieMap(programIdList);
public Map<Long, TicketCategoryAggregate> selectTicketCategorieMap(List<Long> programIdList){
    //根据节目id统计出票档的最低价和最高价的集合
    List<TicketCategoryAggregate> ticketCategorieList = ticketCategoryMapper.selectAggregateList(programIdList);
    //将集合转换为map,key:节目id,value:票档
    return ticketCategorieList
            .stream()
            .collect(Collectors.toMap(TicketCategoryAggregate::getProgramId, ticketCategory -> ticketCategory, (v1, v2) -> v2));
}

selectAggregateList也是自定义的sql,来统计出每个节目对应的票档最低价和最高价

<select id="selectAggregateList" resultType="com.damai.entity.TicketCategoryAggregate">
    select
        program_id,min(price) as min_price,max(price) as max_price
    from d_ticket_category
    where program_id in
    <foreach collection='programIdList' item='programId' index='index' open='(' close=')' separator=','>
        #{programId,jdbcType=BIGINT}
    </foreach>
    group by program_id
</select>

当这些数据都查询出来后,接下来就是进行组装了

组装主页列表需要的数据

//将节目结合按照父节目类型id进行分组成map,key:父节目类型id,value:节目集合
Map<Long, List<Program>> programMap = programList.stream()
        .collect(Collectors.groupingBy(Program::getParentProgramCategoryId));
//循环此map
for (Entry<Long, List<Program>> programEntry : programMap.entrySet()) {
    //父节目类型id
    Long key = programEntry.getKey();
    //节目集合
    List<Program> value = programEntry.getValue();
    List<ProgramListVo> programListVoList = new ArrayList<>();
    //循环节目集合
    for (Program program : value) {
        ProgramListVo programListVo = new ProgramListVo();
        BeanUtil.copyProperties(program,programListVo);
        //演出时间
        programListVo.setShowTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                .filter(list -> !list.isEmpty())
                .map(list -> list.get(0))
                .map(ProgramShowTime::getShowTime)
                .orElse(null));
        //演出时间(精确到天)
        programListVo.setShowDayTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                .filter(list -> !list.isEmpty())
                .map(list -> list.get(0))
                .map(ProgramShowTime::getShowDayTime)
                .orElse(null));
        //演出时间所在的星期
        programListVo.setShowWeekTime(Optional.ofNullable(programShowTimeMap.get(program.getId()))
                .filter(list -> !list.isEmpty())
                .map(list -> list.get(0))
                .map(ProgramShowTime::getShowWeekTime)
                .orElse(null));
        //节目最高价
        programListVo.setMaxPrice(Optional.ofNullable(ticketCategorieMap.get(program.getId()))
                .map(TicketCategoryAggregate::getMaxPrice).orElse(null));
        //节目最低价
        programListVo.setMinPrice(Optional.ofNullable(ticketCategorieMap.get(program.getId()))
                .map(TicketCategoryAggregate::getMinPrice).orElse(null));
        programListVoList.add(programListVo);
    }
    ProgramHomeVo programHomeVo = new ProgramHomeVo();
    //节目类型名
    programHomeVo.setCategoryName(programCategoryMap.get(key));
    programHomeVo.setProgramListVoList(programListVoList);
    //节目列表
    programHomeVoList.add(programHomeVo);
}
return programHomeVoList;

将节目列表按照父节目id进行分组,组装成programMap,接着循环此map,再循环map的value的集合

在循环中,将这些数据组装上去,组装后放在programHomeVoList中,泛型为ProgramHomeVo

ProgramHomeVo结构

@Data
@ApiModel(value="ProgramHomeVo", description ="节目主页列表")
public class ProgramHomeVo implements Serializable  {
    
    private static final long serialVersionUID = 1L;
    
    @ApiModelProperty(name ="categoryName", dataType ="String", value ="类型名字")
    private String categoryName;
    
    @ApiModelProperty(name ="categoryId", dataType ="Long", value ="类型id")
    private Long categoryId;
    
    @ApiModelProperty(name ="programListVoList", dataType ="array", value ="节目列表")
    private List<ProgramListVo> programListVoList;
}

可以看一下组装后给前端返回的数据:

{
    "code": "0",
    "message": "",
    "data": [
        {
            "categoryName": "演唱会",
            "categoryId": "1",
            "programListVoList": [
                {
                    "id": "4",
                    "title": "葡萄不愤怒乐队十周年远走高飞 2024巡演",
                    "actor": "葡萄不愤怒乐队",
                    "place": "MAO Livehouse北京",
                    "itemPicture": "https://s21.ax1x.com/2024/06/06/pkYENcD.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-07-20 19:00:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "199",
                    "maxPrice": "299"
                },
                {
                    "id": "10",
                    "title": "【乐队的春天】超燃乐队现场滚烫开燥|蹦迪开火车|室内音乐节式LIVE| 乐队迷大聚会!HOT乐队歌单",
                    "actor": "",
                    "place": "CLUB HEAVEN (天堂超市世茂工三店)",
                    "itemPicture": "https://s21.ax1x.com/2024/06/06/pkYE5En.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-08-08 20:00:00",
                    "showDayTime": "2024-08-08 00:00:00",
                    "showWeekTime": "周四",
                    "minPrice": "159",
                    "maxPrice": "199"
                },
                {
                    "id": "14",
                    "title": "2024【真的爱你】致敬BEYOND·黄家驹演唱会重现91殿堂级演唱会",
                    "actor": "",
                    "place": "MAO Livehouse北京",
                    "itemPicture": "https://s21.ax1x.com/2024/06/07/pktiAc4.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-07-20 18:30:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "139",
                    "maxPrice": "189"
                },
                {
                    "id": "22",
                    "title": "暗杠「走歌人2024」巡演",
                    "actor": "",
                    "place": "WeShow Live",
                    "itemPicture": "https://s21.ax1x.com/2024/04/30/pkF5T54.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-07-24 20:00:00",
                    "showDayTime": "2024-07-24 00:00:00",
                    "showWeekTime": "周三",
                    "minPrice": "499",
                    "maxPrice": "599"
                },
                {
                    "id": "36",
                    "title": "张学友60+巡回演唱会",
                    "actor": "张学友",
                    "place": "国家体育馆",
                    "itemPicture": "https://s21.ax1x.com/2024/06/07/pkYz139.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-07-22 20:30:00",
                    "showDayTime": "2024-07-22 00:00:00",
                    "showWeekTime": "周一",
                    "minPrice": "480",
                    "maxPrice": "2280"
                },
                {
                    "id": "2",
                    "title": "韦礼安「一直都在」音乐会",
                    "actor": "韦礼安",
                    "place": "JDG英特尔电子竞技中心",
                    "itemPicture": "https://s21.ax1x.com/2024/06/06/pkYEY9K.png",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-08-02 20:00:00",
                    "showDayTime": "2024-08-02 00:00:00",
                    "showWeekTime": "周五",
                    "minPrice": "288",
                    "maxPrice": "488"
                },
                {
                    "id": "6",
                    "title": "「万朵」—春日玫瑰2024新专辑巡演",
                    "actor": "春日玫瑰",
                    "place": "疆进酒·OMNI SPACE",
                    "itemPicture": "https://s21.ax1x.com/2024/06/06/pkYEDAI.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "1",
                    "programCategoryName": "演唱会",
                    "parentProgramCategoryId": "1",
                    "parentProgramCategoryName": "演唱会",
                    "showTime": "2024-08-05 20:30:00",
                    "showDayTime": "2024-08-05 00:00:00",
                    "showWeekTime": "周一",
                    "minPrice": "156",
                    "maxPrice": "299"
                }
            ]
        },
        {
            "categoryName": "话剧歌剧",
            "categoryId": "2",
            "programListVoList": [
                {
                    "id": "15",
                    "title": "俄罗斯芭蕾国家剧院《天鹅湖》",
                    "actor": "",
                    "place": "天桥剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWozc9.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-07-16 20:30:00",
                    "showDayTime": "2024-07-16 00:00:00",
                    "showWeekTime": "周二",
                    "minPrice": "375",
                    "maxPrice": "415"
                },
                {
                    "id": "17",
                    "title": "泱泱国风 民族经典 中国歌剧舞剧院鸿篇巨制舞剧《李白》",
                    "actor": "",
                    "place": "天桥艺术中心-大剧场 ",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWoXhF.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-07-20 18:30:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "158",
                    "maxPrice": "358"
                },
                {
                    "id": "5",
                    "title": "话剧《苏堤春晓》田沁鑫导演",
                    "actor": "辛柏青",
                    "place": "国家话剧院-剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFW69mQ.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-08-02 20:00:00",
                    "showDayTime": "2024-08-02 00:00:00",
                    "showWeekTime": "周五",
                    "minPrice": "258",
                    "maxPrice": "458"
                },
                {
                    "id": "7",
                    "title": "话剧《夜行者》",
                    "actor": "李乃文&唐旭",
                    "place": "首都剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWRugx.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-07-20 19:00:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "288",
                    "maxPrice": "388"
                },
                {
                    "id": "9",
                    "title": "杨丽萍导演作品 新东方美学舞剧《平潭映象》",
                    "actor": "",
                    "place": "北京展览馆剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWRIZF.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-08-05 20:30:00",
                    "showDayTime": "2024-08-05 00:00:00",
                    "showWeekTime": "周一",
                    "minPrice": "258",
                    "maxPrice": "458"
                },
                {
                    "id": "11",
                    "title": "【沉浸式互动戏剧】新年大戏|侦探悬疑喜剧《完美犯罪》环境式爱笑—法庭剧场演出",
                    "actor": "",
                    "place": "雍和宫喜剧剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWRjsK.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-07-26 18:30:00",
                    "showDayTime": "2024-07-26 00:00:00",
                    "showWeekTime": "周五",
                    "minPrice": "580",
                    "maxPrice": "780"
                },
                {
                    "id": "13",
                    "title": "法语原版音乐剧《唐璜》",
                    "actor": "",
                    "place": "天桥艺术中心-大剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWWPJA.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "2",
                    "programCategoryName": "话剧歌剧",
                    "parentProgramCategoryId": "2",
                    "parentProgramCategoryName": "话剧歌剧",
                    "showTime": "2024-08-08 20:00:00",
                    "showDayTime": "2024-08-08 00:00:00",
                    "showWeekTime": "周四",
                    "minPrice": "225",
                    "maxPrice": "325"
                }
            ]
        },
        {
            "categoryName": "体育",
            "categoryId": "3",
            "programListVoList": [
                {
                    "id": "41",
                    "title": "利雅得胜利中国行【C罗来啦】",
                    "actor": "",
                    "place": "体育场",
                    "itemPicture": "https://s21.ax1x.com/2024/05/21/pkMug76.png",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-07-20 20:00:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "480",
                    "maxPrice": "880"
                },
                {
                    "id": "45",
                    "title": "世界摩托车越野锦标赛",
                    "actor": "",
                    "place": "国际越野赛车场 ",
                    "itemPicture": "https://s21.ax1x.com/2024/06/12/pkUIgv8.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-08-06 13:00:00",
                    "showDayTime": "2024-08-06 00:00:00",
                    "showWeekTime": "周二",
                    "minPrice": "1500",
                    "maxPrice": "1500"
                },
                {
                    "id": "21",
                    "title": "乒乓球大赛",
                    "actor": "",
                    "place": "海淀体育场",
                    "itemPicture": "https://s21.ax1x.com/2024/07/07/pkWN3Yn.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-07-21 19:30:00",
                    "showDayTime": "2024-07-21 00:00:00",
                    "showWeekTime": "周日",
                    "minPrice": "300",
                    "maxPrice": "300"
                },
                {
                    "id": "33",
                    "title": "羽毛球大赛",
                    "actor": "",
                    "place": "北京体育馆",
                    "itemPicture": "https://s21.ax1x.com/2024/07/07/pkWtUIA.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-08-01 20:00:00",
                    "showDayTime": "2024-08-01 00:00:00",
                    "showWeekTime": "周四",
                    "minPrice": "700",
                    "maxPrice": "700"
                },
                {
                    "id": "35",
                    "title": "世界斯诺克大师赛",
                    "actor": "",
                    "place": "北京体育馆",
                    "itemPicture": "https://s21.ax1x.com/2024/07/06/pkWSCEq.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-07-11 20:00:00",
                    "showDayTime": "2024-07-11 00:00:00",
                    "showWeekTime": "周四",
                    "minPrice": "900",
                    "maxPrice": "900"
                },
                {
                    "id": "37",
                    "title": "中国男子五人制足球超级赛",
                    "actor": "",
                    "place": "体育场",
                    "itemPicture": "https://s21.ax1x.com/2024/05/21/pkMCoXF.png",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-07-15 20:00:00",
                    "showDayTime": "2024-07-15 00:00:00",
                    "showWeekTime": "周一",
                    "minPrice": "258",
                    "maxPrice": "458"
                },
                {
                    "id": "39",
                    "title": "2024中超联赛北京国安主场赛事",
                    "actor": "",
                    "place": "工人体育场",
                    "itemPicture": "https://s21.ax1x.com/2024/05/21/pkMPC0H.png",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "3",
                    "programCategoryName": "体育",
                    "parentProgramCategoryId": "3",
                    "parentProgramCategoryName": "体育",
                    "showTime": "2024-07-16 20:00:00",
                    "showDayTime": "2024-07-16 00:00:00",
                    "showWeekTime": "周二",
                    "minPrice": "580",
                    "maxPrice": "780"
                }
            ]
        },
        {
            "categoryName": "儿童亲子",
            "categoryId": "4",
            "programListVoList": [
                {
                    "id": "23",
                    "title": "大型全景多媒体舞台剧《冰雪女王Ⅰ艾莎之星梦奇缘》",
                    "actor": "",
                    "place": "北京展览馆剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/21/pFfrfII.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-27 20:00:00",
                    "showDayTime": "2024-07-27 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "410",
                    "maxPrice": "410"
                },
                {
                    "id": "27",
                    "title": "百老汇正版互动科学剧《物理秀》",
                    "actor": "",
                    "place": "北京展览馆剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/21/pFfrdaR.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-29 19:30:00",
                    "showDayTime": "2024-07-29 00:00:00",
                    "showWeekTime": "周一",
                    "minPrice": "350",
                    "maxPrice": "350"
                },
                {
                    "id": "31",
                    "title": "儿童音乐舞台剧《神探艾小坡》",
                    "actor": "",
                    "place": "未来剧院",
                    "itemPicture": "https://s21.ax1x.com/2024/03/22/pFfTpqI.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-08-01 20:00:00",
                    "showDayTime": "2024-08-01 00:00:00",
                    "showWeekTime": "周四",
                    "minPrice": "258",
                    "maxPrice": "258"
                },
                {
                    "id": "25",
                    "title": "沉浸互动益智儿童剧神探爆梗岛之【小小福尔摩斯】全景互动|悬疑推理",
                    "actor": "",
                    "place": "雍和宫喜剧剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/21/pFfrsxO.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-28 19:00:00",
                    "showDayTime": "2024-07-28 00:00:00",
                    "showWeekTime": "周日",
                    "minPrice": "430",
                    "maxPrice": "430"
                },
                {
                    "id": "43",
                    "title": "童艺出品沉浸式体验小剧场儿童剧《丑小鸭》",
                    "actor": "",
                    "place": "海淀剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/07/06/pkRzf3D.jpg",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-21 15:00:00",
                    "showDayTime": "2024-07-21 00:00:00",
                    "showWeekTime": "周日",
                    "minPrice": "2000",
                    "maxPrice": "2000"
                },
                {
                    "id": "19",
                    "title": "东映动漫大型舞台剧《屁屁侦探之怪盗U对怪盗U》",
                    "actor": "",
                    "place": "北京展览馆剧场",
                    "itemPicture": "https://s21.ax1x.com/2024/03/20/pFWTFAK.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-20 19:30:00",
                    "showDayTime": "2024-07-20 00:00:00",
                    "showWeekTime": "周六",
                    "minPrice": "358",
                    "maxPrice": "358"
                },
                {
                    "id": "29",
                    "title": "小猪佩奇舞台剧《完美的下雨天》",
                    "actor": "",
                    "place": "未来剧院",
                    "itemPicture": "https://s21.ax1x.com/2024/03/22/pFfTPdP.webp",
                    "areaId": "2",
                    "areaName": "北京",
                    "programCategoryId": "4",
                    "programCategoryName": "儿童亲子",
                    "parentProgramCategoryId": "4",
                    "parentProgramCategoryName": "儿童亲子",
                    "showTime": "2024-07-30 20:00:00",
                    "showDayTime": "2024-07-30 00:00:00",
                    "showWeekTime": "周二",
                    "minPrice": "230",
                    "maxPrice": "230"
                }
            ]
        }
    ]
}

5. MyBatis 动态 SQL 实现

核心技术:Union All 实现分组 TopN

<select id="selectHomeList" resultType="com.damai.entity.Program">

    <!-- 循环父节目类型ID集合 -->
    <foreach collection='programListDto.parentProgramCategoryIds'
             item='parentProgramCategoryId'
             separator=' union all '>

        select * from (
            select
                dp.id, dp.area_id, dp.program_category_id,
                dp.parent_program_category_id, dp.title, dp.actor,
                dp.place, dp.item_picture
            from d_program dp
            where dp.program_status = 1

            <!-- 地区条件 -->
            <if test='programListDto.areaId == null'>
                and dp.prime = 1  <!-- 全部地区只查主节目 -->
            </if>
            <if test='programListDto.areaId != null'>
                and dp.area_id = #{programListDto.areaId}
            </if>

            and dp.parent_program_category_id = #{parentProgramCategoryId}
            limit 7

        ) as tmp

    </foreach>

</select>
UNION_ALL_分组限制技巧-dcbfb54e

五、设计要点总结

设计要点总结-65033233
  • 读写分离与分层架构
    • ES 负责复杂的检索和聚合,承担绝大部分读流量。
    • DB 负责事务性的写操作和 ES 失效时的兜底读取。
  • 解决 N+1 问题的典范
    • 在 DB 兜底逻辑中,没有在循环 Program 列表时去查 ShowTimePrice
    • 而是先收集所有 ID,通过 **内存(Java Map)**中进行匹配。这是高性能代码的标配写法。
  • SQL 技巧
    • 使用 UNION ALL + LIMIT 子查询,巧妙解决了“在一个 SQL 中查询多个分组的 Top N 数据”的难题,避免了多次数据库网络 IO。
  • 防御性编程
    • try-catch 包裹 ES 查询逻辑,确保搜索引擎挂了不影响主业务。
    • Optional 处理数据组装,防止因脏数据导致的空指针异常。

企业级项目导航:⬅️ 01-Elasticsearch 学习笔记 | 02-主页数据显示讲解 | ➡️ 00-VibeCoding