宠物交流平台11811复盘:Spring Boot全栈开发、表结构与踩坑记录
前阵子整理旧代码重新翻了翻自己做的那个宠物交流平台项目内部编号一直是“11811”所以后来大家叫它“宠物平台11811”。这个项目核心解决的是养宠人群的线上交流需求遛狗找不到伴、宠物生病无处问、猫粮狗粮信息全靠翻旧帖整个圈子缺一个把宠物主按养宠类型、品种、地理位置组织起来的交流空间。做完之后社交类产品常见的那套用户体系、内容动态、互动、私信、匹配推荐基本都完整过了一遍。这套东西对两类人最有用一类正在选课题却不知道从哪下手的学生可以直接参考里面的功能拆解和表结构设计另一类是刚入门的开发者想完整做一个带前后端、带数据库、能跑起来也能部署的项目这个复盘基本把从零到上线的路径写清楚了。下文没有保留完整源码但把架构选型、关键表结构、核心接口实现思路、至少五个踩过之后才解决的坑全部记录下来。照着这个思路自己搭一套难度不大。1. 项目概述与需求定位1.1 宠物交流平台到底在解决什么问题养宠物的人其实有个很明确的痛点信息分散。朋友圈偶尔有人晒猫但你要问“我家猫这几天总吐正常吗”翻遍朋友圈也没有稳定回答。宠物医院又远又贵宠物店老板推荐的东西也不一定靠谱。宠物交流平台的本质是把散落在各个群的宠物主集中到一个统一场景里围绕“宠物档案、动态分享、附近匹配、群聊或私信”这几件核心事做整合。我拿到这个项目的时候第一反应不是先写代码而是先把用户故事列出来。平台里有几类角色普通养宠用户、多宠家庭用户、以及只浏览不发布内容的路人游客。用户画像不同行为差异也很大。普通养宠用户发动态、评论点赞、私信约遛狗多宠家庭用户更关心科学喂养和医疗经验游客则以浏览精选内容为主。需求定位不清晰后面所有功能设计都会飘。所以我把平台的价值主张收敛成三点找到同类问得及时约得方便。1.2 功能清单与目标用户画像功能规划阶段我按“信息展示层、互动层、关系层、系统层”四层拆。信息展示层是宠物档案、动态流、宠物知识库互动层是点赞、评论、转发、收藏关系层是关注、私信、附近的人或宠物推荐系统层是用户管理、举报审核、后台数据统计。每一层对应一个模块模块之间用统一的用户ID串起来。实际开发中我把MVP版本的功能收敛到这些注册登录、宠物档案增删改查、动态发布与图片上传、动态评论点赞、关注与粉丝、附近宠物推荐、一对一私信、系统通知。至于直播、商城、付费课程这些花哨功能全部不做。对于一个交流平台来说先把“内容—互动—关系”这个闭环跑通比堆功能重要得多。后面要扩展底层的表结构也能支撑住。1.3 编号11811背后的小事这个编号其实就是项目创建时自动生成的一个序号后来一直留着没改。很多团队的内部系统都有这种编号习惯从1到9999项目多了一目了然。这里想说的是做项目时给工程起一个稳定可识别的名字比频繁改名的成本低很多。比如数据库名统一叫pet_portal_db项目代号就叫11811后端服务名就叫pet-server微服务拆分的坑在这个规模下没必要踩。2. 技术选型与整体架构设计2.1 后端框架组合Spring Boot 配 MyBatis-Plus后端技术栈我直接选了Spring Boot 2.7 MyBatis-Plus 3.5理由非常简单这个组合在中小型项目里生态最成熟招人成本低文档多碰到问题搜一圈基本都有答案。有人说做小程序项目用Node.js也行但考虑到宠物交流平台涉及用户权限、内容审核、数据统计这类偏后台的业务Spring Boot的生态更顺手。MyBatis-Plus相比原生MyBatis省掉了大量重复的CRUD代码。比如宠物档案的列表查询只需要在Mapper接口里继承一个BaseMapper基础的单表操作全都自带我只需要写那些真正有业务逻辑的SQL比如附近宠物推荐、动态热度的复杂查询。分页也交给MyBatis-Plus的Page对象配合拦截器直接在XML里写带条件的查询。选型的时候还考虑过JPA但如果你是做这种带复杂查询、表关系需要显式控制的系统MyBatis-Plus在SQL可控性上确实更稳。JPA适合标准化极强的领域模型但在这个项目里会遇到很多多条件动态查询还是MyBatis的灵活性更舒服。2.2 前端方案小程序端为主Vue3 管理后台为辅前端是整个项目的重要一环。我的选择是uni-app因为它能一套代码同时编译到微信小程序、H5和App。实际业务里宠物交流平台的主力用户大概率在手机上浏览和互动微信小程序传播成本最低所以主端必须做小程序。管理后台单独做一个Web系统用Vue3 Element Plus负责用户管理、动态审核、数据看板。这里有个很重要的经验不要把前端框架选得太花哨给自己增加不必要的学习成本。uni-app基于Vue3语法虽然是跨端方案但模板语法和普通Vue项目差别不大。你只要把这个项目当成Vue3来写把编译目标设成微信小程序就能跑通。我在实际开发中只用到了一小部分跨端条件编译大部分时间还是写标准Vue3代码。2.3 整体架构与部署方案整个系统采用经典的单体分层架构没有强行上微服务。模块划分是controller层接收参数service层处理业务mapper层访问数据库common模块放工具类和统一返回结构config模块放配置类。从工程结构上就能看出来这是一个标准的单体应用但代码组织会直接影响后面维护成本。部署上用了三台资源云服务器 云数据库 对象存储。后端代码打成jar包用Docker部署在服务器上反向代理交给Nginx前端小程序代码上传到微信平台后编译发布。对象存储用来存放用户上传的宠物图片和视频服务器上只留数据库和代码。之所以要用对象存储是因为用户上传的文件不能放在服务器本地磁盘——服务器磁盘扩容麻烦而且Nginx做静态文件代理对图片处理的能力很弱线上环境维护成本太高。3. 数据库设计与核心表结构3.1 用户表与宠物档案表数据库是整个系统的根基。设计阶段我先梳理了用户和宠物的关系一个用户可以养多只宠物但发布动态的时候需要指定是哪只宠物。于是就有了两个核心表user表和pet_profile表。user表字段包括id、手机号、密码哈希、昵称、头像URL、性别、所在城市、纬度、经度、注册时间、最后登录时间、账号状态。手机号是唯一的密码只存BCrypt加密后的哈希值明文密码绝对不能入库。纬度和经度单独存是给附近宠物推荐功能用的后面会详细讲。pet_profile表字段包括id、用户ID、宠物名、宠物类型、品种、年龄、性别、是否绝育、是否接种疫苗、性格标签、照片URL、简介、创建时间、更新时间。性格标签我推荐用逗号分隔存储因为标签数量少而且不固定单独建表反而增加关联复杂度。两张表关联时在pet_profile上建了(user_id, status)的联合索引这样查询某个用户的所有宠物会很高效。如果你打算给宠物做“默认宠物”的概念再加一个is_default字段每次发布动态时优先选中默认宠物。3.2 动态、评论与点赞关系表动态流是宠物交流平台的核心内容载体。post表的关键字段id、用户ID、宠物ID、正文内容、图片URL列表、视频URL、可见范围、点赞数、评论数、创建时间、状态。图片URL列表我直接用JSON字符串存储比如[https://cdn.example.com/1.jpg, https://cdn.example.com/2.jpg]。有人担心JSON字段不好查询实际上动态列表只需要按创建时间排序根本不需要查询某张图片的URL所以JSON存储没问题。评论表comment字段id、动态ID、评论用户ID、被回复用户ID、父评论ID、评论内容、创建时间。这里支持两级评论父评论ID不为空时表示这是一条嵌套回复。点赞表like_record字段id、动态ID、用户ID、创建时间、唯一索引(用户ID, 动态ID)。这个唯一索引是防止重复点赞的底线保障哪怕并发请求同时进来数据库层面也会拦截住。点赞数和评论数为什么要在post表里冗余存一份因为动态列表页需要高频展示这两个数如果每次都去count性能会随着数据量增大明显下降。维护方式是在点赞或评论操作时同步更新post表的计数。这里面有并发一致性隐患后面排查问题时会专门写到。3.3 私信会话与通知表私信功能不能简简单单只做一张消息表否则会话列表页的研发会很痛苦。我的设计是两张表conversation表和message表。conversation表保存每个会话的概要信息会话ID、发起用户、接收用户、最后一条消息内容、最后一条消息时间、接收方未读数、会话状态。message表保存真正的消息内容消息ID、会话ID、发送用户、接收用户、消息类型、文本内容、是否是图片消息、读取状态、创建时间。会话列表页只需要查conversation表通过最后一条消息和时间排序不需要做子查询。未读数也冗余在会话表上每次打开会话就清零。这个设计在单聊场景下简单而高效。如果后面要做群聊再增加group_conversation表也不冲突。3.4 经纬度字段与查询索引设计实现附近宠物推荐核心问题是“怎么快速找到我附近3公里范围内的宠物”。这里有一个我优化过的方案如果每次查询都把所有用户经纬度拉出来再用Haversine公式计算距离并排序在数据量超过几万之后性能必然撑不住。更好的做法是利用MySQL的空间索引或近似范围查询。我实际采用的是经纬度范围预筛法。用户表里已经存了纬度lat和经度lng。查询附近宠物时先根据当前位置计算出目标经纬度范围当前纬度加减距离因子当前经度加减距离因子除以余弦修正值把范围缩到一个宽高约6公里的矩形框。然后在这个范围内用更精确的Haversine公式计算实际距离并过滤。执行查询时用(lat, lng)联合索引快速定位到矩形框内的记录再把精确计算放到内存里。这样即使数据量到了十万级别查询响应也能稳在百毫秒以内。4. 核心功能实现与实操细节4.1 登录注册、JWT鉴权与密码加密登录注册是每个系统的入口这里容易出错的点在于安全策略。我采用的是手机号 密码方式注册登录生成JWT令牌作为后续请求的凭证。JWT令牌包含三部分Header、Payload、Signature。服务端用某种密钥对Signature签名客户端每次请求携带Authorization请求头。我在Payload里只存userId和loginTime不存手机号等敏感信息。令牌有效期设置成7天实际使用时如果间隔超过这个时间需要重新登录。如果要做更安全的体验可以拆成短期的accessToken和长期的refreshToken但在小项目里一套7天Token完全够用。密码加密用的是BCrypt它比MD5和SHA系列安全得多——即使两个用户密码一样加密后的结果也不同因为加入了随机盐。密码的校验也是用BCrypt的方法完成不要自己写字符串拼接比对。写登录接口的时候我加了一个细节登录成功后返回给前端的用户信息中密码字段一定要置空。很多初学者容易把数据库查询出来的实体对象直接返回这是非常危险的密码哈希值一旦泄露到浏览器端被脱库后用户在其他平台的密码也会遭殃。4.2 动态发布与图片上传流程上传图片的流程不是随便选个文件就传上去。我的实现分三步小程序端先调用上传接口把图片传到对象存储对象存储返回一个可访问的完整URL前端再把URL列表和动态正文一起提交到发布动态接口。对象存储的权限设置要特别注意。宠物交流平台的图片是公开可访问的否则别人看不到你发的宠物照片。但是写入权限必须用服务端签发的临时凭证控制不能让小程序端永久持有上传权限。我当时的方案是后端提供接口去请求临时上传凭证过期时间很短前端拿到凭证后再上传文件。这样就算凭证泄露影响范围也有限。图片处理还是一个不可忽略的细节。用户手机里拍的照片随便都是两三MB直接传上去会很浪费CDN流量。我在对象存储侧设置了图片压缩和缩略图样式规则原来的图压缩到最大1000px列表页用的缩略图是400px的webp格式。前端做展示时根据场景拼接不同参数。4.3 附近宠物推荐距离计算与SQL落地附近宠物推荐是整个项目最有辨识度的功能我在3.4小节里已经提过范围预筛的大思路这里把实现步骤完整写出来。第一当前用户要能获取自己的经纬度。小程序端调用其自带的定位能力获取位置权限被拒时用默认坐标兜底。第二根据距离范围设置上下限。我提供“3公里、5公里、10公里、全部”四挡默认5公里。第三生成SQL查询。实际SQL长这样用伪SQL说明SELECT p.*, u.nickname as user_nickname, u.avatar as user_avatar, ROUND(6371 * 2 * ASIN(SQRT( POWER(SIN((#{userLat} - ABS(u.lat)) * PI() / 180 / 2), 2) COS(#{userLat} * PI() / 180) * COS(ABS(u.lat) * PI() / 180) * POWER(SIN((#{userLng} - u.lng) * PI() / 180 / 2), 2) )), 2) AS distance FROM pet_profile p LEFT JOIN user u ON p.user_id u.id WHERE u.status 1 AND u.lat BETWEEN #{minLat} AND #{maxLat} AND u.lng BETWEEN #{minLng} AND #{maxLng} ORDER BY distance ASC LIMIT 20这段SQL里把精确距离放到查询列表里计算又把范围筛选放到WHERE中利用索引完成粗筛。测试下来在20万条用户数据的情况下这个查询的耗时在150ms以内表现可以接受。有一个坑需要提醒纬度不能直接取绝对值在南半球或者靠近赤道时会不准确。但国内场景基本不存在这个问题所以像我这样写ABS也没事。如果你面向全球化设计就需要严谨对待。坐标的存储统一使用小数点后六位的double类型避免float精度不足导致的范围漂移。4.4 点赞、收藏、关注防重复与计数一致性点赞功能看起来简单做到高并发下不出问题才有挑战。我一开始的时候只做了“先查是否存在再决定插入或删除”的逻辑。后来发现两个请求同时进来查到的结果都是不存在然后都执行插入数据库的唯一索引立刻报了Duplicate entry错误。解决办法就是用数据库唯一索引兜底。业务层查询是否已点赞决定页面展示的是“已赞”还是“未赞”真正执行插入时即使并发重复最多也是抛出违反唯一约束的异常而我捕获这个异常后直接返回“你已经点过赞了”。删除点赞时注意看影响行数如果影响行数为0说明根本没点过赞直接返回原状态。计数一致性是另一个单独的问题。post表里有like_count字段每次点赞成功就update like_count like_count 1。这样做在小流量下没问题但要是某个动态突然火了每一次请求都去更新这一行数据数据库会产生锁竞争。优化方案是把计数放到Redis里用INCR命令累加然后定时批量同步到MySQL。但在MVP阶段直接操作数据库加行锁也完全够用。我是在压力测试发现瓶颈之后才做的Redis改造。4.5 私信实时推送与未读消息处理私信要做到实时绕不开推送。小程序端没有后台常驻的Socket但微信自己提供了一套下行消息能力可以把新消息通知推送给用户。我的实现是用户进入小程序后建立WebSocket连接保持心跳。当对方发来私信时后端先写库然后通过WebSocket通知在线用户。如果用户不在线就调用推送接口发一个“你有一条新的宠物交流消息”的系统通知。这里牵扯到一个细节小程序端和应用服务端之间的WebSocket连接不能一直不断移动端网络环境不稳定断线重连是常态。我在前端做了心跳检测和断线重连机制60秒发一次ping15秒没收到pong就重连。重连时带上用户ID和连接标识后端需要做连接去重避免同一个用户开了两个连接导致消息重复推送。未读消息数量不能靠前端自己数。我在conversation表里维护了unread_count字段A给B发消息时B的未读数加1。B打开这个会话时后端的接口把该会话的B未读数清零。会话列表页直接按最后更新时间倒序返回每条会话带一个未读数前端红点展示逻辑非常直观。5. 常见问题与排查笔记5.1 图片上传后访问404上线第一天就有人反馈头像传上去之后访问是404。排查了一圈后发现对象存储的Bucket权限设置成了私有读写后端生成的URL拼接的是临时凭证的地址但是前端直接拿这个URL放在img标签里临时凭证过期后就访问不了。解决办法是设置指定目录为公开读私有写。发布动态时后端返回公开访问的URL上传凭证仍然用临时凭证且只授予该目录的写入权限。同时加了一条规定所有回传给前端的图片URL统一走公开域名不带签名参数避免临时凭证暴露在浏览器端。5.2 Redis缓存与数据库一致性导致脏数据做动态热度排行时我用Redis缓存了动态列表同时数据库做了点赞数变更。问题来了某用户点赞后动态列表是从缓存读的REDIS里的数据还是旧值出现点赞数少了1、点赞状态却是红心的怪现象。这个问题的根源是缓存和数据库更新顺序不一致。我当时的修正方案很简单粗暴更新数据库之后主动删除Redis里的相关缓存让下一次读取重新回源数据库。后面的请求再去查时会发现缓存没有再去MySQL查一次然后重建缓存。这种方式虽然会多一次磁盘IO但在数据一致性要求高的社交场景里比先更新缓存再同步数据库更可靠。等后面流量上来了再去引入消息队列异步更新现在这个阶段正确性优先。5.3 小程序端的图片无法显示开发环境中图片一切正常才算部署到线上就发现小程序里的图片全挂了Web端却正常。原因是微信公众平台要求所有图片域名配置到“合法域名”列表里对象存储的域名没配置进去。这个问题不难去微信公众平台的后台把对象存储的域名加进去校验一下SSL证书等一段时间同步就恢复了。真正容易踩的是另一个SSL证书过期。对象存储的域名证书过期三个月才被发现那段时间线上图片全挂。后来我把证书到期提醒加入了监控提前30天自动报警。5.4 并发点赞导致的唯一索引冲突这个其实在4.4里已经提到了核心思路这里再补充一个排查经验。线上日志里看到频繁抛Duplicate entry错误第一反应是业务逻辑有重复插入但其实并发场景下唯一索引冲突是预期内的事件。不要把它当成异常去“避免”而是要在业务代码里捕获预期的冲突并静默处理。我的代码是在捕获到唯一约束冲突后执行一次查询并返回“已经点过赞”而不是把异常直接抛出去前端还会误以为网络错误。5.5 经纬度范围查询索引失效刚开始做附近宠物查询时我为了省事写了一个“全表扫描加距离计算排序”的版本数据量到了一万条就已经变慢了。后来加上了lat和lng的联合索引并强制在WHERE里使用BETWEEN范围条件速度提升明显。但有个情况会让这个索引失效如果你把条件写成function(lat) xxxMySQL就不会走索引。所以在查询附近的宠物时记住不要对索引列做任何函数包装直接在原始列上做范围比较。索引应该是(u.lat, u.lng)的联合索引查询时lat和lng都用BETWEEN顺序不要写反。还有一个小细节很多数据库连接池配置错误会导致连接被重置表现为偶发性的查询超时。建议把连接池的validation query设为SELECT 1并配置合理的空闲连接回收时间。6. 复盘总结与后续扩展思路这个项目做完之后我自己最大的感受是一个看似普通的交流平台真正做起来比想象中琐碎。前端要处理上传预览后端要控制权限和字段数据库要兼顾索引与冗余部署上线之后还有一堆环境配置问题。但正因为这么完整地走了一遍后面的其他系统再用到用户、内容、互动这套模型时几乎不用再重新设计。我个人的建议是如果你也想做一个同类项目第一版不要贪功能。把宠物档案、动态、点赞评论、私信、附近推荐这五个主功能做完、做稳已经是一个能答辩、能上线、能写进简历的完整系统了。再往下扩展可以考虑引入消息队列做异步通知拆出推荐服务或者做宠物知识库的搜索引擎。底层表结构预留好这些扩展都不会伤筋动骨。最后再分享一个小技巧项目上线前把每个核心接口用压测工具跑一遍哪怕只是简单的100并发。你会发现很多平时压根想不到的瓶颈比如数据库连接池不够、慢SQL未命中索引、对象存储上传网络超时。这些坑我都是在压测时才暴露出来的。开发完成后先压一轮再上线这个习惯能省掉你好几个深夜的排查时间。