NanE 南易,我把校园互助平台做上线,又亲手把它停了
NanE 是我第一次认真把一个想法做成完整产品。
这里的完整不是写完几个页面,也不是后端接口能在本地返回一段 JSON。它包括需求分析、界面设计、数据库、身份认证、人工审核、服务器部署、HTTPS、域名、日志、异常处理,以及上线之后那个最容易被程序员忽略的问题。
到底有没有人用。
故事开始于一个很普通的宿舍场景。
大学校园里有几千名学生,分散在不同校区、园区和楼栋。几乎每个人的抽屉里都有一些只用过两三次的东西。创可贴、碘伏棉签、退烧贴、螺丝刀、胶带、数据线,甚至是一包拆开后剩下很多的应急耗材。
这些东西平时安静地躺在抽屉里。
与此同时,隔壁房间可能正有人急着找。
理论上,两个人之间只有几十米。现实中,他们互相不知道对方的存在。
宿舍群当然可以求助,但群聊是一条不断向下流动的河。消息发出去之后,有没有人看到、有没有人愿意回复、是不是刚好有人在线,全靠运气。即使成功借到,第二天这条信息也沉底了,无法搜索,无法复用,更无法形成稳定的互助关系。
我当时就在想,能不能把这种偶然发生的互助,变成一种结构化的信息撮合。
于是有了 NanE,中文名叫南易。
它不做买卖,不做配送,也不提供医疗建议。平台只负责展示经过审核的信息,让真正有需要的人更容易找到附近愿意分享的人。物品是否适合使用,包装是否完整,有效期是否正常,仍然需要领取者自己判断。
这条边界非常重要。
如果一开始没有想清楚边界,一个校园互助平台很容易在需求膨胀中变成二手交易平台、跑腿平台、药品平台和即时通讯软件的混合体。最后什么都想做,什么都做不好,还可能带来不必要的安全责任。
我为什么故意不用前端框架
NanE 的网页端使用原生 HTML、CSS 和 JavaScript。
没有 React,没有 Vue,没有 Vite,也没有一长串看起来很专业的构建插件。连 CSS 都是自己写的,按照设计令牌、基础样式、布局和组件拆成几个文件。
后端同样克制。
我用了 Node.js,但没有使用 Express,直接基于原生 http 模块实现服务器、路由分发、请求体解析、静态文件服务和 JSON 响应。
这听起来有点像给自己找罪受。
事实上,也确实是在给自己找罪受。
使用 Express 的话,路由、中间件、错误处理和请求解析都有成熟方案。原生 http 模块只会给我一个请求对象和一个响应对象,剩下的事情都要自己处理。路径怎么匹配,请求体多大时拒绝,JSON 解析失败怎么办,接口异常后如何确保只返回一次响应,这些问题一个都不会自动消失。
但我仍然选择了这种方式。
NanE 的业务规模并不大。我不希望为了十几个核心页面和一组校园互助接口,引入一套需要持续升级的前端工程体系。我更希望几年后重新打开仓库时,仍然能直接看懂页面是怎么加载的,事件是怎么绑定的,数据又是怎么流动的。
不用框架不代表把所有代码塞进一个文件。
前端仍然被拆成视图、接口客户端、校验工具、弹窗、提示、骨架屏和动画模块。后端也有独立的认证中间件、限流逻辑、物品路由、领取路由、上传服务和数据库模块。
所谓轻量,不是少写几个文件。
轻量应该意味着依赖关系足够清楚,启动过程足够简单,维护者不用先考古半小时才能修改一个按钮。
没有 GPS,我就自己定义什么叫附近
校园互助最核心的问题不是物品总数,而是排序。
同样是一盒创可贴,隔壁寝室的显然比另一个校区的更有价值。传统生活服务平台通常依赖 GPS 距离,但校园宿舍场景不适合这么做。
一方面,网页定位权限会吓退用户。另一方面,宿舍内部的 GPS 精度并不稳定。更重要的是,我根本不需要知道一个人的精确坐标。
知道校区和楼栋就够了。
于是 NanE 使用了一套离散的近邻排序规则。
同楼栋排在最前面。
同宿舍群排在第二。
同校区排在第三。
其他校区放在最后。
同一优先级内部,再按照发布时间倒序排列。
真正麻烦的是「同宿舍群」怎么定义。仙林、浦口和苏州校区的宿舍命名方式并不完全相同,有的是数字楼栋,有的是园区加甲乙丙丁。我最后为不同校区分别维护归一化和分组规则,把用户填写的各种括号、空格、横线和大小写先清理掉,再计算所属宿舍群。
这段代码不复杂,却非常像真实产品里的代码。
它没有炫目的算法,也写不进算法竞赛题解,但它把模糊的校园生活经验转化成了可执行规则。
PWA 让我离原生应用近了一点
NanE 最初以网页端为主,但我希望用户在手机上使用时,不要总觉得自己打开了一个临时网页。
于是我加入了 PWA 支持。
Web App Manifest 负责描述应用名称、图标、启动方式和主题颜色。用户可以把 NanE 添加到手机桌面,从图标直接进入,视觉上更接近普通应用。
Service Worker 则负责缓存应用外壳。
HTML、CSS、JavaScript、字体和图片使用缓存优先策略。接口请求使用网络优先策略,网络失败时再尝试读取缓存。这样即使校园网突然抽风,已经打开过的页面框架仍然可以显示,而不是只留下一张浏览器错误页。
这里也踩过一个很典型的坑。
Service Worker 太能缓存了。
我修改完 CSS,刷新页面,界面毫无变化。我开始怀疑路径、Nginx 和浏览器,最后发现是旧缓存还活得好好的。后来我只能给缓存名称加版本号,每次修改核心资源时主动清理旧缓存。
有时候程序真正忠实执行你的设计,反而更让人生气。
安全不是加一个登录页面
校园项目很容易让人放松警惕。
大家都是同学,好像就不需要太严格的安全设计。但联系方式、宿舍楼栋和领取记录都属于需要谨慎处理的信息。
NanE 的基本原则是游客可以浏览,但不能直接看到联系方式。用户完成身份验证并补全资料后,才可以主动查看发布者留下的联系方式。
我早期评估过 Supabase Auth。它接入快,邮箱认证也比较成熟,很适合验证产品原型。但随着南大邮箱验证码、南哪认证代理和自定义账户流程逐渐明确,最终仓库采用的是自己的认证接口、南大邮箱验证和签名 JWT。
所有物品不会发布后立即公开。
用户提交的信息先进入待审核状态。管理员可以通过、驳回、下架或批量处理。登录接口有基础限流,上传文件会检查类型,敏感接口需要用户或管理员身份,真实环境变量也不会提交进仓库。
我并不觉得这些设计让 NanE 变得绝对安全。
安全从来不是一个完成状态。它只是让我在能力范围内,尽量减少那些本来可以提前避免的问题。
从本地运行到真的上线
项目最后部署在 Azure VM 上。
外层是 Nginx,负责 HTTPS、域名和反向代理。Node.js 服务由 PM2 管理,PostgreSQL 保存用户、物品、领取记录和审核数据。证书通过 Let’s Encrypt 配置,网页曾经运行在 nane.zylatent.com。
第一次看到自己写的页面通过正式域名打开时,我确实兴奋了很久。
然后真正的运维开始了。
数据库连接失败、环境变量写错、端口被占用、Nginx 配置没有重新加载、证书路径不对、PM2 重启后没有带上新变量。每一项单独看都不难,但它们会在你准备睡觉的时候排队出现。
我也写了微信小程序端,准备在备案和相关流程完成后继续推进。小程序代码后来一直保留在仓库里,不过项目的主要交付仍然停留在网页端。
再后来,我把服务停了。
不是因为代码彻底坏了。
相反,主要流程已经可以完整跑通。从发布、审核、展示、查看联系方式,到提醒领取、确认履约和双方评价,技术上已经形成了闭环。
真正的问题是运营。
校园产品需要宣传,需要第一批供给,需要持续审核,需要回答用户问题,还需要让大家相信这里真的有人回应。如果首页只有三件物品,用户来一次没有找到需要的东西,可能就再也不会回来。
我一个人可以凌晨修接口,却很难一个人同时完成地推、审核、社群维护和内容运营。
最终,NanE 因为运维精力和推广困难暂停了。服务器可以重新启动,代码也完整保留,但它没有成为我最初想象中的长期校园基础设施。
代码之外的那一半
以前我觉得,一个产品最困难的部分是把代码写出来。
做完 NanE 之后,我发现代码可能只占一半。
剩下的一半是需求边界、信任、冷启动、推广、审核、维护和真实用户的耐心。程序员很容易沉迷于继续增加功能,因为写功能比面对「没有人使用」更舒服。
NanE 没有持续运营下去,这是一段诚实的失败经历。
但我并不后悔。
这是我第一次独立负责一个产品从需求到上线的完整链路。我学会了设计 API,拆分前后端模块,维护数据库,配置服务器,处理身份认证,也第一次认真考虑隐私、审核和风险边界。
更重要的是,我学会了在写第一行代码之前先问一句。
这个产品上线以后,谁来让它继续活着。
NanE 没有走到我原本想象的终点,但它把我送到了下一个项目的起点。
