广场动态推荐改造复盘:一套「配置化 + 热点召回」的信息流架构

周一早上,运营在群里问:广场能不能把调仓动态也放进推荐流,下午就要上。旧逻辑按时间序写死,改一次展示就要发一次版。内容写入、召回、配置、清理由四拨人各管一段,这次改造把五个环节串成一条链路。
本篇你能学到
- 用一张轻量索引表收口多类动态召回,避免直查业务大表。
- 用
biz_type加ext_config的 JSON 配置,把展示形态从发版里解耦。 - 按用户状态分流召回,给没有行为数据的冷启动用户留时间降序兜底。
- 用穷举表格核对跨 4 个组件 7 个写入入口,避免推荐池缺内容。
- 给索引表配每日清理任务,控制表体积。
一、整体链路:五个环节串成一条信息流
广场模块要支持动态推荐能力:不再按单一时间序吐内容,而是把分享、调仓、发帖等各类动态按类型组织起来,运营侧还能灵活调整展示形态。
这类需求容易把注意力全放在推荐算法上。真正决定上线质量和迭代速度的,往往是工程侧的结构:内容怎么进池、怎么召回、怎么展示、怎么治理。
1. 五个环节分别解决什么问题
这次改造的核心改动五条:
- 新增业务开关配置表,支持广场功能开关与展示类型配置化;
- 新增广场热点召回索引表,提升热点内容召回效率;
- 新增动态推荐接口,根据用户状态使用不同召回策略;
- 分享、调仓、发帖等内容写入时同步生成热点索引数据;
- 新增定时任务清理过期索引数据。
把五条改动放回业务流程,就是一条从写入到清理的链路:
内容写入
→ 同步生成热点索引
→ 推荐接口按用户状态分流召回
→ 开关配置控制展示
→ 每日清理过期索引每个环节都有明确的交付物:写入环节保证内容进池,索引环节保证召回够快,分流环节保证不同用户看到合适的内容,配置环节保证运营能调整,清理环节保证索引表不膨胀。
配图

图 1:推荐五环节,内容写入、热点索引、状态分流、开关配置、每日清理
2. 为什么把链路拆成五段
推荐类需求最常见的失败,不是算法不够好,而是链路缺一段。写入漏一个入口,池子里就少一类内容;召回直查业务大表,接口就会被拖慢;配置写死在代码里,运营每改一次展示都要排期。
拆成五段之后,每一段都可以单独验收。写入看入口清单,召回看索引表,分流看用户状态,配置看开关表,清理看定时任务。出了问题也能快速定位是哪一段断的。
3. 预期结果
上线后,分享、调仓、发帖等动态都能按类型进入推荐池。登录且已加圈的用户看到按类型轮询的内容,游客和未加圈用户看到时间降序内容。运营调整展示类型不需要发版,过期索引每天 01:00 自动清理。
二、两张核心表:把“会变的”挪出代码
推荐流有两个东西天然会变:展示形态和热点内容。前者变化频繁,后者有保质期。用两张表分别承接,变化就不会传导到代码里。
1. 开关配置表:展示配置不写死
t_biz_switch_config 承担业务开关与扩展配置,动态展示类型配置放在 ext_config 字段里。
这里有个值得说的选择:展示配置用 JSON 扩展列而不是结构化列建模。理由有三:
- 展示配置天然是嵌套结构(总开关 → 类型子开关 → 排序方式),拍平成关系列会很别扭;
- JSON 列加新配置项不需要改表结构;
- 配置调整不需要发版,改数据即生效,推荐流这种“展示形态要反复试”的功能,把发版从迭代环里摘出去,价值非常大。
同一张表通过 biz_type 区分业务域(搜索栏 SEARCH_BAR、动态列表 DYNAMIC_LIST),一个配置模式服务多个功能模块,后续接新的展示位不用重复建表。
搜索栏配置(biz_type = SEARCH_BAR):
id=1 搜索栏展示总开关
├── id=2 动态-搜索框(分享/调仓/话题/发帖)
├── id=3 圈子-搜索框
└── id=4 关注-搜索框动态列表配置(biz_type = DYNAMIC_LIST):
id=9 动态列表展示总开关
├── id=10 分享
├── id=11 调仓
├── id=12 话题
└── id=13 发帖两级结构:总开关控制功能入口,类型级开关控制每种动态是否展示。一次配置同时决定广场展示哪些类型的内容。
排序逻辑上还做了一个预留:PlazaServiceImpl#buildSortTemplate 把排序模板从主流程中拆分出来,后续支持后台动态配置。排序规则是推荐流里最常被调整的部分,独立成模板,等于给“策略会变”这件事留好了口子。
2. 热点召回索引表:召回不直查业务大表
t_plaza_hot_recall_index 是整个方案的查询核心。设计上的取舍在于:召回逻辑是直接查业务表,还是单独建一张轻量索引表?
直查业务表的问题很实际:各类动态分散在不同的业务表里,字段多、行宽大,只为召回却要扛出整行数据;查询要跨表 UNION,逻辑越写越散;过期内容没法独立清理。
单独建索引表之后,三个问题同时解决:只存召回必需字段,查询轻快;所有类型的动态统一收口到一张表,召回逻辑收敛在单表;过期数据按时间整批清理,完全不碰业务数据。
3. 预期结果
展示配置全部落在 ext_config,运营改数据即可调整搜索栏和动态列表。召回查询只扫 t_plaza_hot_recall_index,不再跨业务大表 UNION。新增展示位时复用同一张开关表,按 biz_type 扩展。
三、召回策略:按用户状态分流
推荐策略的前提是用户行为数据。没有数据时的“个性化”是瞎猜,分流的意义就在于承认这一点。
1. 推荐接口与分流规则
新增的推荐接口 /option/plaza/dynamicRecall,核心分流逻辑只有一张表:
| 用户状态 | 召回策略 | 排序规则 | 设计意图 |
|---|---|---|---|
| 登录且已加圈 | 按类型轮询召回 | 默认或配置排序 | 保证内容多样性 |
| 未登录或未加圈 | 时间降序召回 | 生产时间倒序 | 保证内容时效性 |
登录且加了圈的用户,我们有他的兴趣偏好,可以讲究多样性;游客和没加圈的用户没有任何行为数据,任何“个性化”都是瞎猜。这种时候,时间倒序反而是最稳的策略,至少保证用户看到的是最新内容。
2. 分流逻辑怎么写
接口层只做一件事:判断用户状态,选择对应召回策略。伪代码如下。
recall(userId):
若 已登录 且 已加入圈子:
return 按类型轮询召回(排序 = 默认或配置排序)
否则:
return 按生产时间降序召回()这也是冷启动场景的通用二分法:有行为数据才上策略,没行为数据就上时间。分流条件放在接口入口,后续新增用户分层时只改这一处判断,不用动两套召回实现。
3. 预期结果
登录且已加圈用户的首屏能同时看到分享、调仓、话题、发帖等类型,不会连续刷出同一种内容。游客和未加圈用户按生产时间倒序看到最新内容,冷启动不再出现“猜错兴趣”的推荐。
四、写入链路与清理:让索引表持续可用
索引表要有人写、有人清。写入漏入口,池子缺内容;清理不配,轻量表迟早膨胀。
1. 写入链路:跨 4 个组件 7 个入口
这次改造里工程量最大、也最容易漏的环节是写入链路:所有内容生产入口都要同步写热点索引,漏一个,推荐池里就缺一类内容。
按内容类型穷举如下。
分享类(内容组件):
| 接口/入口 | 说明 |
|---|---|
/api/app/decision/tool/share/execute | 决策工具分享 |
/community/strategyTradingOrderShare | 策略交易分享 |
调仓类(定时任务组件):
| 入口 | 说明 |
|---|---|
ShareChangeServiceImpl#genTrendingTrade | 股票调仓动态生成 |
FundsMessageProcessServiceImpl#saveMessage | 基金调仓动态生成 |
发帖类(社区核心组件 / 社区管理后台):
| 组件 | 写入时机 | 核心入口 |
|---|---|---|
| 社区核心组件 | AI 审核通过 / 人工审核通过 / 帖子发布到圈子 | AuditStateMachine#fireEvent |
| 社区管理后台 | 帖子上架 | CommunityPostServiceImpl#insertPostRecallIndex |
两条经验值得单独说。
第一,写入时机挂在“过审”之后。帖子的索引写入挂在 AI 审核 / 人工审核通过、上架之后,而不是用户提交时。这样保证进推荐池的内容都是合规内容。如果挂在提交时,池子里就可能混进后来被审核拒绝的内容,还得再做反向清理。
第二,跨组件的入口必须穷举。本例的写入入口跨 4 个组件 7 个入口,代码分散在不同团队手里。这种范围靠“记忆和感觉”必然漏,唯一靠谱的办法就是像上面三张表一样按内容类型列表格、逐个入口确认。穷举表格本身就是方案交付物的一部分。
2. 清理任务:索引表的最后一环
热点索引是“有保质期”的数据,过期的热点不再是热点。配套的清理任务 PlazaHotRecallIndexDataClearJob 每天 01:00 执行,清理过期的召回索引。
PlazaHotRecallIndexDataClearJob(每天 01:00):
扫描 t_plaza_hot_recall_index 中已过期的记录
按时间条件批量删除这一环经常被忽视:不配清理任务,“轻量索引表”迟早膨胀成没人敢碰的大表,建索引表时换来的查询优势会被慢慢吃掉。执行时间选凌晨低峰期,对白天业务零影响。
3. 预期结果
分享、调仓、发帖三类内容从各自的入口写入后,都能在推荐池里被召回。过审前的内容不会提前进池,被驳回的内容不需要反向清理。索引表每天凌晨清掉过期数据,查询性能保持稳定。
常见错误与排错
- 现象:某类动态在推荐流里始终不出现。原因:写入入口漏了,或者索引写在了用户提交时。解法:按内容类型列出 4 个组件 7 个入口逐个核对,写入时机统一挂到审核通过或上架之后。
- 现象:推荐池里出现已被驳回的帖子。原因:索引在提交时就写入,审核拒绝后没有反向清理。解法:把写入后移到 AI 审核、人工审核通过、上架之后。
- 现象:索引表越来越大,召回查询逐渐变慢。原因:没有配置过期清理任务。解法:启用
PlazaHotRecallIndexDataClearJob,每天 01:00 清理过期记录。 - 现象:游客刷到的内容点击率反而低。原因:对没有行为数据的用户也走了类型轮询。解法:未登录或未加圈统一走时间降序,登录且已加圈才按类型轮询。
- 现象:运营调整广场展示类型要排期发版。原因:展示配置写死在代码里。解法:把配置放进
t_biz_switch_config.ext_config,按 SEARCH_BAR / DYNAMIC_LIST 区分业务域,改数据生效。
本篇小结
- 五个环节要逐段验收:内容写入、热点索引、状态分流、开关配置、每日清理。
- 召回走
t_plaza_hot_recall_index,不直查业务大表。 - 展示配置放
ext_config,按biz_type复用同一张开关表。 - 写入入口按内容类型穷举,时机挂在过审之后。
- 索引表配每日清理,避免轻量表膨胀成大表。