从一张地图开始:赭山生活地图的开发记录
这不是一个试图覆盖所有城市的点评产品,而是一份由同学共同维护的校园生活档案。
安徽师范大学赭山校区周边有不少值得去的店,但这些信息通常散落在聊天记录、朋友圈和学长学姐的口头推荐里。于是我做了一个小型的校园生活地图,希望把“哪里值得去”这件事变得更直观,也让后来的同学可以继续补充内容。
我想解决什么问题
这个项目最初并不是为了做一个复杂的点评平台,而是想完成三件简单的事:
- 以赭山校区为中心,在地图上看到周边的美食和娱乐地点。
- 点开地点后,能看到学长学姐写下的具体推荐理由。
- 让用户可以匿名投稿,再由管理员审核,保证公开内容的基本质量。
因此,地图是首页的主体,推荐抽屉默认隐藏,用户可以先看位置,再决定是否打开详细列表。地点标记也没有使用传统的大图钉,而是用颜色区分类型的圆点,让地图保持干净。
地图首页只展示已经审核通过的地点,投稿内容不会直接进入公开地图。
技术选型
前端使用 Vite、React 和原生 CSS。React 负责地图、抽屉、投稿表单和管理后台之间的状态切换,Vite 则提供快速的开发和构建体验。
地图最开始尝试过 Leaflet 和 OpenStreetMap,但在校园周边的文字标注、道路和建筑信息上,实际体验不够理想,尤其是在移动端和较高缩放级别下。因此后来切换到高德地图 JavaScript API,并保留了自己的圆点标记和界面图层。
数据和身份系统使用 Supabase:
places保存地点、坐标、推荐理由和审核状态。categories保存可配置的地点分类。detail_fields保存营业时间、价格、适合场景等详情字段。- Supabase Storage 分别保存待审核图片和已公开图片。
- Row Level Security 确保普通用户只能读取已审核地点,管理员才能管理后台数据。
部署使用 Vercel。前端环境变量放在 Vercel,Supabase Edge Function 的服务密钥和 Turnstile 密钥则只保留在 Supabase 服务端环境中。
最终部署链路是:GitHub 保存代码,Vercel 构建前端,Supabase 提供数据库、身份验证、存储和 Edge Function。
地图坐标的一个坑
项目使用的地点数据以 WGS84 经纬度保存,但高德地图使用 GCJ-02 坐标。如果直接把数据库坐标传给高德,地点会出现明显偏移。
因此,数据写入数据库时保留 WGS84,显示到地图前转换为 GCJ-02;用户点击地图投稿时,则把高德返回的坐标转换回 WGS84 再保存。这样可以避免把某个地图服务的坐标格式带进数据层,也方便将来替换地图服务。
这个细节看起来很小,却是地图项目中最容易被忽略、也最影响可信度的一部分。
不要把 WGS84 坐标直接传给高德地图,否则地点通常会出现几十到几百米的偏移。
投稿和审核流程
普通用户点击“投稿地点”,在地图上选择位置后,表单会自动填入经纬度。地名和推荐理由是必填项,其他详情、图片和自定义字段都可以按需填写。
投稿不会直接出现在公开地图上,而是经过下面的流程:
用户选点
-> 填写推荐
-> Turnstile 验证
-> Edge Function 限流与写入
-> pending 待审核
-> 管理员通过
-> approved 出现在地图
图片也采用了分阶段处理:投稿时先放进私有存储桶,管理员审核时生成临时预览地址;审核通过后再复制到公开图片桶。这样可以避免未经审核的图片被公开访问。
为了防止重复提交,Edge Function 会根据 IP 和浏览器指纹做每日限流,目前匿名用户每天最多投稿 5 次。每次投稿都会生成查询码,用户可以用查询码查看审核状态,而不需要注册账号。
投稿后保存查询码即可查看审核状态,不需要注册账号,也不会在公开页面展示投稿人的身份信息。
管理员特权
管理员后台包含待审核、已发布、分类和详情字段四个部分。管理员可以查看投稿地图位置、预览图片、修改名称和推荐理由,并执行通过、驳回、编辑和删除操作。
后来又增加了管理员地图加点功能。管理员登录后可以开启“管理员地图加点”,直接在地图上选择位置,填写信息后立即发布,不需要 Turnstile,也不需要再次经过审核。这适合录入已经确认过的店铺或补充站点基础数据。
分类和详情字段也没有写死在组件里,而是交给后台配置。分类可以修改名称和颜色,详情字段可以增加、修改或删除。删除分类前会检查是否仍有地点引用,避免因为外键关系造成数据错误。
管理员删除分类前,应先确认没有地点正在使用它;详情字段删除后,历史地点中的原始数据仍保留在数据库中,但不会继续作为默认详情展示。
界面上的取舍
这个站点没有使用传统的后台首页或营销式封面,打开后直接进入地图。界面上只保留搜索、分类筛选、推荐抽屉和地点详情几个核心入口。
窄屏适配也是开发过程中反复调整的一部分。桌面端的品牌区和操作区可以横向排列,但手机宽度不足时,顶部区域会纵向排列,推荐列表变成底部抽屉,详情面板也会自动变成单列布局。
我还给地图左下角的“赭山校区”增加了回到起点的操作。用户在地图上移动、缩放或查看某个地点后,可以随时回到校园中心。
在移动端,推荐列表会变成底部抽屉,顶部品牌区和操作区也会纵向排列,以避免按钮互相挤压。
一些开发体会
这个项目规模不大,但包含了地图坐标、文件存储、权限控制、审核状态和移动端布局等多个容易出问题的部分。开发过程中最重要的经验有三点:
第一,数据结构要早于页面确定。分类和详情字段一开始就做成配置数据,后面增加管理功能时就不需要重写表单。
第二,公开数据和管理数据必须分开考虑。普通用户只需要看到 approved 地点,管理员则需要看到 pending 地点和私有图片,RLS 和 Storage 权限应该成为系统的一部分,而不是靠前端隐藏按钮来实现。
第三,错误信息不能只显示“请求失败”。Edge Function 返回非 2xx 时,前端应该尽量透传服务端的具体原因,否则排查 Turnstile、限流和数据库错误会非常困难。
下一步
目前赭山生活地图已经可以支持匿名投稿、管理员审核和动态配置。后续还可以继续完善:增加地点去重提示、补充路线和距离信息、支持更多校园区域,以及在移动端进一步优化图片和详情展示。
项目的推荐数据不写在前端代码里,管理员可以通过后台维护分类、详情字段和地点内容。
这不是一个试图覆盖所有城市的点评产品,它更像是一份由同学共同维护的校园生活档案。希望每一个后来来到赭山校区的人,都能少走一点弯路,多发现几家真正值得去的店。