从分区到匿名:我如何在校园论坛中用“一个字段”实现两个功能

0 阅读5分钟

本文记录了一个大二学生在开发校园论坛时,实现帖子分区和树洞匿名功能的完整过程。两个功能看似不同,本质上却是同一种设计模式——给数据模型加一个属性,然后根据属性值决定不同的行为。

前言

我的校园论坛(江农论坛)已经上线运行了一段时间,有了基础的帖子发布、评论互动、游客模式等功能。但作为一个面向全校学生的平台,所有帖子堆在一起显然不行——选课交流、二手交易、校园活动、吐槽树洞混在一起,浏览体验很差。

今天的目标很明确:实现帖子分区功能。顺便把“树洞匿名”也做了——让树洞分区的帖子可以匿名发布。

做完之后回头一看,发现这两个功能本质上是一回事:都是给 Post 模型加一个属性,然后前端根据属性值给不同的反馈。 这篇文章就记录这个“举一反三”的过程。

一、分区功能:从数据库到 UI 的全链路

1.1 数据模型:一个 category 字段解决所有问题

最开始我犯了一个错误思路——想给每个分区建一个独立的 MongoDB Model(StudyPostLifePostTradePost……)。后来被提醒,这样做会导致跨分区搜索、首页聚合都变得复杂,而且帖子结构明明一样,分 Model 是冗余设计。

正确做法很简单:在现有的 Post Schema 里加一个 category 字段,用 enum 限定可选值:

// models/Post.js
category: {
  type: String,
  required: true,
  enum: ['study', 'life', 'trade', 'other'],
  default: 'other'  // 旧帖子自动归入树洞
}

四个分区:学习交流(study)、校园生活(life)、二手交易(trade)、树洞(other)。default: 'other' 保证了数据库里现有的旧帖子不会因为缺少字段而报错,自动归入树洞。

1.2 后端接口:同一套代码,加个筛选条件

GET /api/posts 接口原本无条件返回所有帖子。现在只需要在查询前构建一个筛选对象:

const filter = {}
if (req.query.category) {
  filter.category = req.query.category
}
const posts = await Post.find(filter)
  .populate('author', 'name')
  .populate('comments.author', 'name')
  .sort({ createdAt: -1 })

不带 ?category=study 就返回全部,带了就按分区筛选。一个接口,两种行为,由查询参数决定。

POST /api/posts 接口也只需要多接收一个 category 字段,存入数据库即可。

1.3 前端 Store:给 fetchPosts 加个参数

async function fetchPosts(category) {
  let url = '/api/posts'
  if (category && category.trim() !== '') {
    url += `?category=${category}`
  }
  const res = await fetch(url)
  // ...
}

addPost 同理,多传一个 category 字段。

1.4 前端 UI:导航栏 + 下拉菜单

首页加了一个分区导航栏,按钮数据来自一个 tabs 数组:

const tabs = [
  { key: '', label: '全部' },
  { key: 'study', label: '学习交流' },
  { key: 'life', label: '校园生活' },
  { key: 'trade', label: '二手交易' },
  { key: 'other', label: '树洞' },
]

点击按钮 → switchCategory(key)fetchPosts(key) → 页面更新。发帖页面加了一个分区下拉菜单,数据源和导航栏一致。

这里踩了一个样式坑:原生 <select> 下拉框在深色模式下样式割裂,选项列表是浏览器默认样式,和论坛的圆润风格完全不搭。后来用自定义下拉组件替代了原生 <select>,用 <div> + <ul> + <li> 模拟选项,样式完全可控。


二、树洞匿名:复制粘贴分区功能的思路

2.1 同一个设计模式

分区功能做完后,树洞匿名几乎是“复制粘贴”同一个思路:

步骤分区功能树洞匿名
数据模型category 字段anonymous 字段
后端接收接收 category,存入数据库接收 anonymous,存入数据库
后端返回category 筛选如果 anonymous=true,隐藏作者名
前端传参下拉菜单选分区勾选框选匿名
前端展示按分区显示不同列表匿名帖子显示“匿名用户”

两个功能的本质一模一样:给模型加一个属性,后端根据属性值做不同处理,前端根据属性值做不同渲染。

2.2 具体实现

Post Schema 加字段:

anonymous: {
  type: Boolean,
  default: false,
}

后端 GET 接口返回时处理匿名:

const result = posts.map(post => {
  const postObj = post.toObject()
  if (postObj.anonymous) {
    postObj.author.name = '匿名用户'
  }
  postObj.comments.forEach(comment => {
    if (postObj.anonymous && comment.author) {
      comment.author.name = '匿名用户'
    }
  })
  return postObj
})

核心逻辑:数据库里永远存真实作者 ID(方便管理员溯源),展示层根据 anonymous 决定是否替换名字。

前端发帖组件加勾选框:

<label v-if="category === 'other'">
  <input type="checkbox" v-model="anonymous" /> 匿名发布
</label>

只在用户选择“树洞”分区时才显示,其他分区不出现。

评论自动继承帖子的匿名属性:

post.comments.push({
  comment: comment,
  author: req.user._id,
  anonymous: post.anonymous  // 继承帖子的匿名状态
})

不需要用户额外操作,在树洞帖子下发的评论自动匿名。如果树洞帖子本身是实名的,评论也实名。


三、一个意外的 Bug:文字溢出

做完这两个功能后,手机端访问时发现文本会超出卡片边界。排查后发现,原来的 CSS 没有加 overflow: hiddenword-break

修复很简单,给帖子内容和评论正文加上:

word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
overflow: hidden;

这个 Bug 之所以存在,是因为项目初期代码由 AI 生成骨架,当时没有考虑到长文本溢出的边界情况。这也印证了一个经验:AI 生成的代码能帮你快速搭建功能,但最终的视觉走查和边界测试必须自己过一遍。


四、总结:从“会写”到“会抽象”

这次实现分区和匿名功能,我最大的收获不是学会了某个技术点,而是开始理解“抽象”的意义。

分区的 category 和树洞的 anonymous,本质上都是给数据对象增加元信息,然后在不同层级根据元信息做不同处理。这个模式适用于很多场景——标签、置顶、精华、审核状态……本质上都是一个字段 + 条件判断。

下次再做类似功能,我不会再从零开始想,而是直接套这个模式:数据模型加字段 → 后端接收 + 条件处理 → 前端传参 + 条件渲染 → 样式适配。


项目状态更新:

  • 已完成功能:帖子发布、评论互动、游客模式、深浅主题切换、分区浏览、树洞匿名
  • 下一步:首页推荐排序、个人主页

如果你也在独立做全栈项目,欢迎评论区交流你的踩坑经历。