梦想城镇列表页搜索与「进行中」筛选重构复盘(2026-09-02)

今天做了什么

这两天在梦想城镇小程序的列表页上花了不少时间。这个列表页是镇长互助社区的核心入口:用户发求助帖、按关键词搜自己缺的卡牌或产品,再进详情帮别人。问题有两个,用户反馈得很直接——搜得很不准,而且不少早就过期的求助还挂在「进行中」里能点。

我把它当成一次典型的「查询质量」重构来做,而不是头痛医头。下面把过程、坑和方法论都记下来,也方便以后面试时拿出来讲。

搜索为什么不准

最初的 buildWhere 只匹配了 titlegameId 两个字段。可真实数据里,卡牌名藏在 haveCards[].name / wantCards[].name,产品物品名在 items[].name,正文在 content。搜「火车票」「小麦」这些词,压根没进索引,自然空手而归。

修法第一步是扩展字段,用云开发的 _.or 一并匹配:

const kw = escapeRegexp(keyword.value.trim())
buildWhere = _.or([
  { title: db.RegExp({ regexp: kw, options: 'i' }) },
  { content: db.RegExp({ regexp: kw, options: 'i' }) },
  { gameId: db.RegExp({ regexp: kw, options: 'i' }) },
  { 'items.name': db.RegExp({ regexp: kw, options: 'i' }) },
  { 'haveCards.name': db.RegExp({ regexp: kw, options: 'i' }) },
  { 'wantCards.name': db.RegExp({ regexp: kw, options: 'i' }) },
])

第二步是正则转义。原来的 database.RegExp({ regexp: keyword }) 没转义,用户输入 (*[? 这类字符,云库查询直接报错。加一行兜底:

function escapeRegexp(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

第三步是触发方式,这个来回改了两轮。一开始做成「输入即搜」防抖 300ms,用户反馈输入过程中列表乱跳;回退成「回车才搜、清空才刷新」;后来用户又要求改回输入即搜。最终版是输入停顿 300ms 才查库,清空关键词立刻回全部,回车则立即搜并清掉防抖计时器——既避免每敲一字就跳,又不让回车被防抖串扰。

一个很隐蔽的 Taro 4 坑

这里踩到一个容易忽略的坑:Taro 4 的小程序 input 组件,如果同时写 v-model 和显式 @inputkeyword 会不同步。表现就是输入「小」字,列表闪一下却显示全部——因为 buildWhere 读到的是空 kw,直接回了全部。

修复办法是模板去掉 v-model,改成 :value="keyword",在 onSearchInput(e) 里显式取值:

onSearchInput(e) {
  keyword.value = (e.detail.value) || ''
}

小程序 input 的值路径是 e.detail.value,不是 e.target.value。这是 Taro 4 + Vue3 小程序受控输入的可靠写法,以后这类 input 一律这么写,别依赖 v-model

过期帖过滤与可选字段兜底

再说过期帖。原来「进行中」只按 status:'open' 过滤,可产品帖过期后 status 还是 open,于是继续挂在列表里还能帮。我用 conditions 数组统一拼装:

if (filterStatus === 'open') {
  conditions.push({ status: 'open' })
  conditions.push(_.or([
    { expiryAt: _.exists(false) },   // 旧数据缺字段
    { expiryAt: _.eq(0) },          // 卡牌帖永久有效
    { expiryAt: _.gt(Date.now()) }, // 产品帖未过期
  ]))
}

这里出了一次回归。第一版我只写了 _.eq(0)_.gt(now),结果卡牌帖从「进行中」整类消失。排查发现,「过期时长」功能上线前发布的卡牌帖,以及部分未设时长的旧产品帖,数据库里根本没有 expiryAt 字段,值是 undefined_.eq(0) 匹配不到 undefined_.gt(now)undefined 也无从比较,于是被一并过滤。补 _.exists(false) 后,语义变成「字段不存在 / 等于 0 / 大于 now」三者任一都算有效,只排除「产品帖 expiryAt 落在 (0, now]」的区间。微信云开发的 db.command.exists 原生支持。

白屏事故

还有个白屏:groupByTheme 重构时漏加到 setupreturn 里,模板直接调用拿到 undefined,整页白屏。教训很朴素:模板里直接调用的纯函数,必须在 setup return 显式导出,import 不够。

方法论沉淀

这几件事看着零散,背后是同一套工程素养:

  • 列表查询的质量,取决于是否真实覆盖字段分布和数据演进(旧数据可能缺字段)。
  • 查询条件集中到 buildWhereconditions 数组拼装,比到处散写稳得多,也方便统一加正则转义与状态过滤。
  • 对可选字段永远假设它可能缺失,用 _.exists(false) 兜底,是低成本高收益的写法。
  • Taro 4 + Vue3 小程序里 input 受控一律 :value + @input 显式同步,不碰 v-model

顺带一句,镇长互助这个模块后来因为微信社区类目下架了,但这套列表查询和筛选逻辑是通用的,换个项目照样能复用。

如果你想看更多这类工程复盘,可以来我的博客 https://blog.alleria.cn 翻翻,或者去 GitHub 仓库翻我的技术笔记。