本地活动网站看起来很容易做。
放几张海报,写上时间地点,再给一个报名二维码,页面就能上线。但真正麻烦的部分通常不在页面上,而在页面背后:活动信息从哪里来,谁来维护,图片怎么传,结束后的相册怎么放,过期活动怎么处理,发布前怎么确认没有破坏线上版本。
CityPlayer 一开始面对的就是这个问题。它不是要做一个漂亮的活动列表,而是要给米兰华人和留学生的本地活动提供一个稳定的信息承接系统。公开站点是 cityplayer.top,前台负责活动发现和报名触达,后台负责活动创建、编辑、发布、隐藏、删除、图片上传和相册维护。
这类项目最容易做错的地方,是把它当成静态展示页。
静态页的第一版通常很快。问题是,活动不是静态内容。每一场活动都有时间、地点、价格、状态、封面、二维码、详情正文和相册。信息还经常来自微信群文案、海报文字或临时通知。如果每次都靠手工改页面,早期看似省事,后面会把维护成本转移给人。
所以 CityPlayer 的核心取舍,是先把内容结构和运营链路稳定下来,再做视觉和渠道扩展。
项目背景
CityPlayer 面向的是一个很具体的场景:米兰本地华人和留学生想知道最近有什么活动,活动组织者需要一个比群公告更稳定的承接页。
这个场景不需要一开始就做复杂社区,也不需要把所有功能塞进一个超级后台。它真正需要的是几件事:
- 活动可以被清楚地展示。
- 活动信息可以被后台维护。
- 报名触达路径要直接。
- 图片和二维码不能在展示时被随意裁掉。
- 活动发布前后要有可验证的状态。
- 后续如果扩展到微信小程序,不能重写整套业务逻辑。
这也是为什么我没有把 CityPlayer 只做成一组静态卡片。静态卡片只能解决“看起来有内容”,不能解决“这个内容以后怎么继续被运营”。
开发旅程:先把运营链路跑通
CityPlayer 的开发不是从“做一个漂亮首页”开始的,而是从确认链路开始的。
先让本地前后端、数据库和上传能力跑起来,才能判断系统现在能做什么、缺什么、哪些地方不能乱动。然后再重写前端,把活动首页、活动详情、后台登录、后台列表和活动编辑页整理进同一套手绘视觉语言,同时保留原来的 API 和上传行为。
这个顺序很重要。视觉可以提升信任感,但如果后台不能稳定创建、发布和维护活动,前台再漂亮也只是一个临时页面。后端审查同样不是为了证明技术栈完整,而是为了确认 public API、admin API、鉴权、发布状态、相册、上传、审计记录和测试能支撑真实运营。
换句话说,CityPlayer 的开发主线不是“先做页面,再补后台”,而是先把运营动作放进系统,再让页面承接这些动作。
技术栈不是重点,边界才是重点
CityPlayer 的 Web 前端使用 React 19、TypeScript、Vite、Tailwind v4、Framer Motion 和 Zustand;后端使用 Node.js、Express、TypeScript、Prisma、PostgreSQL、JWT 和 Zod,部署在 Vercel。
这些技术本身并不稀奇。更重要的是边界。
前端只消费 API,不放后端业务逻辑。它负责活动列表、筛选、活动详情、二维码展示、相册浏览、页面动效和响应式体验。
后端负责数据模型、发布状态、管理员鉴权、上传、相册、审计记录和公开接口。活动主表承接标题、slug、简介、正文、封面、二维码、开始结束时间、时区、地点、价格、发布状态、排序权重和软删除状态。相册独立成表。后台操作有审计记录。
这套结构让项目后续比较容易扩展。比如活动相册不是写死在正文里,而是有独立数据;发布状态不是靠“页面是否存在”判断,而是有明确的 draft、published、hidden;后台上传和公开展示也不会混在一起。
视觉风格也是产品约束
CityPlayer 的前端没有走通用 SaaS 风格,而是保留了统一的手绘视觉语言。
这不是为了装饰。对一个本地活动平台来说,冷冰冰的企业后台感反而会削弱信任。它需要更像一本活动笔记本:纸张质感、不规则边角、硬阴影、手写感字体、轻微旋转的卡片、像贴纸一样的视觉层次。
但这种风格也有风险。手绘不等于随意。每个按钮、卡片、输入框和页面状态都需要遵守同一套视觉规则,否则很容易变成“这里可爱一下,那里随便一下”。
所以我把风格当成工程约束来处理:前端组件要复用,本地设计 token 要稳定,任何外部 UI 或生成代码都必须重新整理到 CityPlayer 的视觉系统里。这样做慢一点,但后面更不容易散。
后台不是越复杂越好
CityPlayer 的后台重点不是做一个完整 CMS,而是覆盖活动运营最常见的动作。
管理员可以创建、编辑、发布、隐藏和删除活动,可以上传封面、二维码和相册。活动详情页会根据状态展示不同提示。结束后的活动可以保留回顾和相册,而不是直接从列表里消失。
后来我加了一个更贴近真实运营的小功能:粘贴信息创建。
很多活动原始信息不是结构化表单,而是一段微信群文案或海报文字。后台提供一个粘贴入口,由后端规则解析标题、简介、时间、地点、价格和内容模块,再自动填入表单。
这里我刻意没有把它包装成全自动发布。规则解析可以减少录入成本,但它不能替代人工复核。系统会给出 notes,提醒年份推断、结束时间缺失、多个价格冲突等不确定点。这个边界很重要:自动化应该减少重复劳动,而不是把错误更快地发布出去。
图片处理是小项目里经常被低估的部分
活动系统里,图片不是“附属内容”。封面图、二维码、相册图决定了用户能不能快速理解活动,也决定了报名路径是否顺畅。
真实活动图片很不规整。有竖版海报,有横图,有二维码,有现场图。如果全部用简单的 object-cover,人物、文字或二维码很容易被裁掉。
所以 CityPlayer 后来单独处理了图片展示策略。后台封面预览、相册缩略图、详情页大封面和二维码预览使用不同的展示逻辑:需要完整识别的二维码用 contain,活动图根据比例选择 cover 或 contain,首页卡片和详情页封面调整裁切重心。
这些不是炫技功能,但它们决定系统能不能被日常使用。很多小产品坏掉,不是因为架构不够高级,而是因为这些“很小但每天会遇到”的细节没人收拾。
上线不是 deploy
CityPlayer 让我更确认一件事:上线不是把代码推到生产环境。
上线至少包括一条验证链。这个项目上线前做了前端 build、后端 build、后端测试、生产依赖 audit、浏览器 smoke check、API smoke check、并发压力测试和生产回读。过程中也修掉了两个看似不大的问题:未登录直达后台编辑页时不应该先打出一次无意义的 401;密码登录页需要补隐藏 username 字段,避免浏览器可访问性提示。
这些问题不一定会让产品不能用,但它们会让系统显得没有收口。对一个需要继续交接和迭代的小系统来说,收口比一次性上线更重要。
后续小程序不是重做一遍
CityPlayer 后续也拆出过微信小程序方向。这里的取舍是:小程序只做前台,不放后台管理、登录和发布能力。
它使用 Taro、React 和 TypeScript,复用线上公开 API:活动首页和活动详情。小程序侧更适合做浏览、搜索、收藏、最近查看和分享,而不是把管理后台搬进去。
这是一个很实用的边界。Web 后台继续作为内容维护入口,Web 公开站和小程序作为不同前台渠道。业务数据不重复,接口可以复用,前台体验可以分别优化。
几个判断
第一,小项目也需要内容模型。越是早期项目,越不能把内容随便写死。写死会让第一版变快,但会让第二版和第三版变重。
第二,后台的价值不是功能多,而是让真实运营少掉临时沟通。活动系统最需要的不是复杂权限,而是让创建、发布、上传、回看这些动作稳定发生。
第三,视觉风格不能只停在设计稿里。只要项目还会迭代,风格就必须变成组件、token 和验收规则。
第四,自动化要有刹车。粘贴识别可以帮人少填很多字段,但最后发布前仍然需要人看一眼。这个审阅点不是低效,而是系统可信的一部分。
第五,上线验证要覆盖生产链路。build 通过只是开始,真实浏览器、真实接口、生产回读和压力测试,才能证明这个系统可以交出去。
CityPlayer 对我来说,是一个很典型的小团队产品样本:需求不大,但足够真实;功能不花哨,但每一步都牵涉维护成本。这样的项目最适合用清楚的结构来做,不需要一开始装成大型平台,也不能只停在漂亮页面。
这也是我现在更关注的工作:把本地社区、内容流程和 Web 产品,整理成一个可上线、可复盘、可继续交接的系统。