尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+微信小程序摄影分享平台开发全流程详解

SpringBoot+微信小程序摄影分享平台开发全流程详解 这两年我陆陆续续帮人改过不少SpringBoot的毕设项目摄影作品分享交流平台是出现频率相当高的一个题目。它的好处在于后端能覆盖登录鉴权、文件上传、列表分页、点赞评论这些经典场景前端又能把微信小程序的授权、图片上传、触底加载这些能力全部串一遍一套项目做完前后端该练的技术点基本都碰到了。这篇文章我就按实际开发顺序把从业务拆解到数据库设计、后端接口、小程序前端、再到联调部署的整套过程展开讲。内容偏实操每一步都讲清楚“怎么做的”和“为什么要这么做”。不管你是拿这个题目做毕设还是想自己从零写一个类似的小程序社区都应该能直接参考。1. 摄影分享平台的业务拆解与整体设计思路1.1 核心功能模块怎么拆才不乱很多人在动手写代码之前容易犯一个错——上来就建表、写接口做到一半才发现评论和消息通知的关系没理清或者作品图片的存储方案根本扛不住数据量。我的习惯是先花半天把业务模块画清楚再谈技术实现。一个摄影作品分享交流平台表面上功能很多但剥开来看就四个核心域用户域微信授权登录、个人资料维护、关注/粉丝关系、我的作品/收藏/点赞列表作品域发布作品图片标题描述标签、作品列表推荐/最新/热门、作品详情、删除作品互动域点赞、收藏、评论含回复、互动消息通知辅助域搜索按标题/标签、敏感词过滤、内容审核标记这里面最容易忽略的是互动消息通知。很多新手做社区类项目会把点赞、评论做出来但“别人赞了我的作品我要不要知道”这个问题经常被跳过。如果消息通知不做那“交流”两个字就是个空壳。哪怕毕设也建议至少做一版简单的通知点赞/评论时往消息表插一条记录小程序端的消息tab拉取未读列表。这块代码量不大但对项目完整度的提升非常明显。另一个容易被忽略的点是图片合规处理。摄影作品分享平台是公开内容图片和文字都需要审核。毕设级别不需要接第三方审核API但至少要做两层第一层是发布时对标题、描述做关键词过滤第二层是在管理后端留一个作品下架入口配合status字段把违规内容标记为不可见。这两项加在一起能避免很多不必要的麻烦。1.2 SpringBoot加小程序这套技术栈怎么选技术选型这件事我见过太多人纠结半天。其实摄影分享平台这个量级的项目选型原则就一条选你最熟、社区资料最多的组合别追求新框架。后端用SpringBoot是顺理成章的。SpringBoot 2.7.x是目前最稳的版本3.x虽然出了很久但部分第三方starter的兼容性偶尔会给人“惊喜”做项目没必要给自己找这个不痛快。持久层我用MyBatis-Plus而不是JPA原因非常实际MyBatis-Plus的QueryWrapper写条件查询很直白稍微复杂点的SQL也方便手写对不熟悉Hibernate关联映射的人来说心智负担小得多。前端小程序这块原生WXML加JavaScript就够了。不要一上来就上uni-app或Taro除非你明确知道以后要同时发布支付宝小程序或抖音小程序。原生开发的调试体验最直接wx.xxx的API调用在官方文档里都有详细说明出问题也好排查。如果以后真要跨端再用uni-app重写也来得及但那是另一个故事了。数据库用MySQL 8.0缓存和搜索引擎在这个量级都不需要。文件存储方面毕设项目可以直接存服务器本地目录通过SpringBoot的静态资源映射暴露访问链接如果服务器带宽有限或者考虑到以后要扩展用云厂商的OSS/COS也是顺手的事下面第3章单独讲。2. 数据库表设计用户、作品、互动三块基石怎么落2.1 用户表、作品表、评论表的字段设计表设计是整个项目的地基地基歪了后面全是坑。我按实际使用的表结构来讲你可以直接参考。用户表user是标准的微信小程序用户模型字段类型说明idbigint主键自增openidvarchar(64)微信openid唯一索引nicknamevarchar(32)昵称avatarvarchar(255)头像URLbiovarchar(255)个人简介gendertinyint性别0未知 1男 2女statustinyint账号状态0正常 1禁用create_timedatetime注册时间update_timedatetime更新时间这里有个关键点openid属于敏感信息后端任何接口返回用户信息时都不能把openid带出去只能在前端拿用户id做标识。我见过有人直接把整个user对象序列化返回前端openid泄露不说反正也是不规范的做法。正确做法是建一个UserVO只包含id、nickname、avatar这些前端展示需要的字段。作品表work是整个系统的核心表字段类型说明idbigint主键user_idbigint发布者ID索引titlevarchar(64)作品标题descriptiontext作品描述cover_urlvarchar(255)封面图URLimage_urlstext所有图片URLJSON数组字符串tagsvarchar(255)标签逗号分隔locationvarchar(255)拍摄地点可空like_countint点赞数冗余collect_countint收藏数冗余comment_countint评论数冗余view_countint浏览量冗余statustinyint0待审核 1正常 2下架create_timedatetime发布时间图片URL用JSON数组字符串存在image_urls里这是很多人会纠结的点。要不要单独建一张作品图片表我的答案是在作品最多九张图的约束下JSON串完全够用还能省掉一次联表查询。前端拿到字符串后JSON.parse一下就行。第一个图作为cover_url单独存一个字段列表页不用每次解析JSON取封面。评论表comment要支持最简单的回复功能字段类型说明idbigint主键work_idbigint作品ID索引user_idbigint评论者IDparent_idbigint父评论ID0表示顶级评论reply_user_idbigint被回复者ID用于通知contentvarchar(512)评论内容create_timedatetime评论时间parent_id字段同时承担了“这条评论是不是回复”和“回复的是哪条评论”两个职责。做楼层回复时只需判断parent_id是否为0非0则查出父评论的内容展示在界面上不需要做成无限层级树那是浪费精力。2.2 点赞、收藏、关注三张互动表的设计取舍互动关系本质上就是“谁对什么做了什么”数据模型非常统一三张表结构几乎一样。点赞记录表like_recordwork_id user_id 联合唯一索引防止重复点赞。 收藏记录表collect_record同样work_id user_id联合唯一。 关注关系表followuser_id关注者 follow_user_id被关注者联合唯一。这三张表的设计要点在于唯一索引一定要建否则前端连点两次按钮就会插两条重复数据。你当然可以在代码里先查再插但数据库的唯一索引才是最后一道保险。至于计数问题我建议直接用作品表里的like_count、collect_count冗余字段。每次点赞/取消点赞时在同一个事务里更新记录表和计数用事务保证一致性。数据量没到百万级之前完全不需要引入Redis做计数缓存反而多一个缓存和数据库的一致性维护成本。如果以后数据量真的大了再考虑用Redis的incr命令但那是后话了。还有一个小技巧作品列表接口需要判断“当前登录用户是否点赞/收藏过”常规做法是查完作品列表后再查一次互动表In查询user_id和work_ids的匹配关系把它们组装进返回对象。不要N条作品查N次互动表那样列表接口基本就废了。3. 后端接口实现登录鉴权与作品上传的完整链路3.1 微信登录加JWT鉴权的闭环微信小程序登录的流程很多文章都写过但实际操作起来有几个细节值得注意。前端调用wx.login拿到临时code把code传给后端。后端拿到code后调用微信的jscode2session接口换取openid和session_key。这里有个新手常犯的错误直接把code拿给后端就完事了却没有意识到jscode2session接口需要appid和appsecret这两个东西是绝对不能出现在小程序前端代码里的。所以“前端传code后端换openid”这个流程是强制要求不是可选项。后端核心代码大致是这个样子PostMapping(/login) public ResultString login(RequestBody LoginRequest req) { // 1. 用code换openid String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code req.getCode() grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject data JSON.parseObject(response); String openid data.getString(openid); // 2. 查用户不存在则注册 User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(摄影爱好者 System.currentTimeMillis() % 10000); user.setStatus(0); userMapper.insert(user); } // 3. 生成JWT返回前端 String token JwtUtil.generateToken(user.getId()); return Result.success(token); }JWT里的payload只放userId和过期时间不放其他敏感信息。token有效期我习惯设成7天小程序不是那种需要长期保持登录的重型应用7天到期后用户重新静默登录一次体验上没有影响。鉴权这块用SpringMVC的HandlerInterceptor实现。写一个LoginInterceptor从请求头Authorization里解析token解析成功就把userId放进request的attribute里后续接口直接从request里取当前用户id。这里我建议用一个自定义注解RequireLogin标记需要登录的接口而不是用路径匹配做白名单——注解的方式更灵活比如作品详情页是公开可看的不用登录但点赞接口必须登录一个注解就能区分开。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod method (HandlerMethod) handler; RequireLogin anno method.getMethodAnnotation(RequireLogin.class); if (anno ! null) { String token request.getHeader(Authorization); Integer userId JwtUtil.parseToken(token); if (userId null) { throw new BizException(401, 未登录); } request.setAttribute(userId, userId); } } return true; } }这个拦截器里我直接用抛异常的方式处理未登录状态配合全局异常处理器转换成401 JSON返回给前端代码比在拦截器里写response输出要干净得多。3.2 图片上传的接口链路摄影作品平台的核心资产是图片所以上传接口要格外仔细。前端的wx.chooseMedia选完图后调用wx.uploadFile把文件以multipart/form-data方式POST到后端。后端接收上传的接口比较简单PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BizException(400, 文件不能为空); } // 1. 校验文件类型和大小 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .webp).contains(suffix.toLowerCase())) { throw new BizException(400, 不支持的图片格式); } if (file.getSize() 10 * 1024 * 1024) { throw new BizException(400, 图片大小不能超过10MB); } // 2. 按日期分目录存储 String date LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String filename UUID.randomUUID().toString().replaceAll(-, ) suffix; String relativePath upload/ date / filename; File dest new File(uploadDir relativePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 3. 生成缩略图 // 用Thumbnailator生成宽400的缩略图列表页展示用 Thumbnails.of(dest).width(400).toFile(dest.getParentFile() /thumb_ filename); return Result.success(/api/file/ relativePath); }这里有两个实践中的细节。第一文件名必须用UUID重命名不能保留原始文件名否则中文文件名和非法字符会让URL变得不可控重名还会互相覆盖。第二列表页和详情页展示的图片尺寸其实不一样——列表页只需要宽400的缩略图就够了详情页才加载原图。生成一张thumb缩略图能显著减少列表接口的流量消耗小程序端滑起来也更快。如果是部署到云服务器之外的对象存储其实逻辑差不多MultipartFile拿到之后直接putObject到OSS/COS拿到返回的URL存到数据库。区别是本地存储要配SpringBoot的静态资源映射把/api/file/路径映射到upload目录对象存储则天然带CDN加速访问速度更好。毕设用本地存储生产环境用对象存储这个思路比较清晰。3.3 首页Feed流的接口设计首页是作品信息流这个接口是系统的门面性能直接影响体验。最基础的实现是分页查询作品表按创建时间倒序GetMapping(/feed) public ResultPageResultWorkVO feed(RequestParam Integer page, RequestParam Integer pageSize) { PageWork p new Page(page, pageSize); LambdaQueryWrapperWork wrapper new LambdaQueryWrapper(); wrapper.eq(Work::getStatus, 1) // 只展示正常状态 .orderByDesc(Work::getCreateTime); PageWork result workMapper.selectPage(p, wrapper); // 转VO补充用户信息、是否点赞收藏 return Result.success(...); }分页参数page和pageSize的设计上page从1开始pageSize固定10或20。前端触底加载下一页直到返回的数据量小于pageSize就说明没有更多了。返回体里同时带一个hasMore字段比前端自己判断更直观。这种limit分页在数据量小的时候没问题但超过十万条数据后offset深翻页会越来越慢。如果考虑到以后扩展可以改成游标分页把排序字段和时间戳拼成一个游标下一页查询时带上where create_time 上一页最后一条的create_time。这个改造不难但做好这层设计能让你少踩很多性能坑。热门榜单是另一个常见需求我建议加一个时间窗口条件只统计近7天发布的作品按like_count倒序。这样榜单会持续更新而不是被老作品永久霸榜。SQL大概是这样SELECT * FROM work WHERE status 1 AND create_time DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY like_count DESC, create_time DESC LIMIT 204. 小程序前端页面交互与关键实现4.1 tabBar页面结构和自定义导航栏适配小程序前端我建议用四个tabBar页面首页、发布、消息、我的。发布页做成tabBar有一个好处用户点一下就能进入发布流程路径最短这对UGC社区的产品逻辑很重要。如果你不想让发布页在tabBar里占一个位置也可以用普通页面加悬浮按钮的方式但tabBar方案更省事。导航栏这块有两个选择用系统默认导航栏或者自定义。摄影社区类应用我强烈建议自定义导航栏因为默认导航栏没法做沉浸式效果首页的轮播图和背景很难和状态栏融合。自定义导航栏的核心是计算状态栏高度和导航栏高度const systemInfo wx.getWindowInfo(); const statusBarHeight systemInfo.statusBarHeight; const navBarHeight statusBarHeight 44; // 44是胶囊按钮所在区域的标准高度不同机型的statusBarHeight不一样iPhone的刘海屏和安卓的挖孔屏数值差异很大所以必须运行时动态获取不能写死。页面里用一个占位view的高度等于statusBarHeight再往下布局内容就不会顶到状态栏。4.2 列表触底加载更多和分页状态管理首页信息流的交互模型是首次进入加载第一页滑动到底部自动加载下一页顶部下拉刷新回到第一页。这三点在微信小程序里都有对应能力。分页状态管理最朴素的做法是在data里维护三个字段data: { list: [], page: 1, hasMore: true, loading: false }onReachBottom触底时先判断hasMore和loading防止重复请求onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.loadData(); } async loadData() { this.setData({ loading: true }); const res await request.get(/feed, { page: this.data.page, pageSize: 10 }); this.setData({ list: this.data.list.concat(res.data.list), page: this.data.page 1, hasMore: res.data.hasMore, loading: false }); }注意列表数据的拼接方式是list.concat而不是直接赋值否则会丢掉已经加载的历史数据。这个逻辑看着简单但我在实际代码review里见过太多次把list直接替换成新页数据的低级失误。还有一个容易被忽略的细节wxml里渲染长列表时图片一定要加lazy-load属性让图片在进入可视区域时才加载。否则首页一次渲染几十张图片首屏性能会非常差。同时image组件的mode用widthFix或aspectFill避免图片拉伸变形。4.3 图片选择和上传的坑微信小程序的图片选择接口wx.chooseMedia比老版的wx.chooseImage功能更完整支持多选、预览和压缩。发布作品时最多选九张图选择阶段就应该把压缩参数带上wx.chooseMedia({ count: 9, mediaType: [image], sourceType: [album, camera], sizeType: [compressed], success(res) { // res.tempFiles拿到选中图片的临时路径 } });sizeType: [compressed]指定使用压缩图能大幅减少上传流量。但这里有个坑compressed压缩后的图片在部分安卓机型上会被压缩得比较狠画质有明显损失。所以如果你的用户对图片质量有要求建议选择compressed版本但后端存储时保留原图URL和压缩图URL两个字段。上传的时序问题我也踩过一次。用户选了九张图后如果九张图同时并发上传服务器压力大不说小程序的wx.uploadFile并发限制也可能导致部分请求失败。稳妥的做法是串行上传维护一个上传队列一张传完再传下一张每张都拿到返回的URL后拼接成一个数组最后连同标题、描述一起调用发布接口。这样虽然慢了一点但可靠性和顺序都有保证。发布接口一次性save整个作品记录不会出现作品已经创建但图片还没传完的中间状态。前端上传代码async function uploadImages(files) { const urls []; for (let i 0; i files.length; i) { const res await new Promise((resolve, reject) { wx.uploadFile({ url: ${baseUrl}/upload, filePath: files[i].tempFilePath, name: file, success: resolve, fail: reject }); }); urls.push(JSON.parse(res.data).data); } return urls; }这里每次循环都await一个Promise把并发变成了串行。九张图上传完可能需要十几秒期间要有一个loading提示否则用户会以为卡死了这是交互体验的基本要求。5. 联调与部署高频问题排查速查5.1 后端常见报错和处理思路做这个项目时后端最容易出问题的几个点我整理成了一份速查表问题现象常见原因处理方案前端调接口报CORS跨域未配置跨域过滤器WebMvcConfigurer里addCorsMappings允许小程序域名上传大文件直接被拒SpringBoot默认上传限制1MB配置spring.servlet.multipart.max-file-size和max-request-size时间字段返回格式是时间戳Jackson默认序列化行为字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)请求报401token过期或未携带前端检查请求拦截器是否加了Authorization头小程序图片403服务器Referer防盗链误伤对象存储配置白名单或关闭referer限制SQL查询慢缺少索引explain分析work.user_id加索引数据库连接池这块也值得提一句。SpringBoot默认的HikariCP开箱即用性能很好。但连接池最大连接数默认是10如果并发高一点偶尔会出现“connection is not available”的报错。在application.yml里把maximum-pool-size调到20再配合合理的请求超时时间基本不会再遇到这个问题。5.2 小程序端高频报错和处理思路问题现象常见原因处理方案真机预览请求失败开发者工具关了“不校验域名”真机仍然校验必须有HTTPS合法域名且在小程序后台配置request合法域名wx.login拿不到code基础库版本过老检查项目基础库版本一般2.10以上没问题图片上传成功但列表不显示URL路径拼接错误后端返回相对路径时前端要拼接完整域名setData报错Data is too large一次setData传了过多数据避免把大图片base64塞进data控制单次setData在1MB以内页面白屏app.json页面路径配置错了检查每个page路径是否与实际文件一致小程序上线还有一道绕不开的坎域名备案。发布正式版时所有请求都必须走HTTPS域名需要ICP备案服务器也需要有公网IP。很多人在开发阶段一切正常一到提交审核就被域名校验卡住。这个问题没有捷径只能提前准备域名和备案或者在大学环境里用已经备案好的域名做代理。5.3 部署上线前必须检查的事项上线前的检查清单我每次都会自己过一遍小程序后台把request合法域名配置好包括接口域名、图片CDN域名多个都要列进去SpringBoot的application-prod.yml里数据库密码用环境变量注入不要硬编码在配置文件里上传目录的磁盘空间要注意摄影社区图片是最大空间消耗者定期清理或者上对象存储前端所有请求的baseUrl切换到线上域名不要带着localhost或127.0.0.1日志级别调成info把错误日志单独输出到文件出问题时有据可查最后再分享一个小经验。整个项目做完以后我建议把首页信息流的响应时间作为核心性能指标。一个摄影社区如果用户刷图片要等两三秒再好的内容体验也是白搭。所以列表接口务必返回缩略图URL而不是原图URL数据库查询务必走索引分页不要贪多。这些细节叠加起来才是用户觉得“这个App很流畅”的真正原因。这个项目做完以后个人体会最深的一点是SpringBoot和小程序各自单独学的时候都觉得自己会了但真正把它们对接起来中间那些跨域、域名校验、上传格式、权限传递的问题才是实际开发最花时间的地方。把这些链路捋顺了以后再做什么社区类小程序基本都能触类旁通。
返回列表