尧图精选

Flask+uniapp打造校园交友微信小程序:从接口设计到推荐算法实战

🕒 发布时间:2026/10/1 17:38:16 📁 来源:尧图网络
去年接了一个校园项目要做一款面向大学生的在线社交交友微信小程序。技术栈最终落在Python Flask uniapp前端一套代码同时覆盖微信小程序和H5后端用轻量级Flask快速迭代。这半年下来踩了不少坑今天把完整实现过程、接口设计思路、匹配推荐算法和小程序端实践一次性整理出来给想自己做类似社交平台的读者一份可以直接参考的实战记录。我会按照从需求拆解、后端设计、小程序端实现、推荐算法到联调上线这条主线来讲。中间涉及的具体代码都贴了关键片段参数和实现逻辑也尽量写清楚方便你照着改。1. 大学生交友平台的需求拆解与技术选型1.1 平台的核心功能面做社交平台第一个教训是别一上来就想做全套。微信、QQ那套功能体系不是学生项目能hold住的我当时和需求方反复对齐最终砍到四个核心模块用户体系微信授权登录、个人资料编辑、兴趣标签选择动态广场发布文字动态可带图片、浏览他人动态、点赞和评论匹配推荐根据兴趣标签相似度推荐可交友的用户会话入口看到感兴趣的人能发起聊天不自己做IM直接跳转到微信客服会话或复制微信号规避即时通信资质问题这四个模块基本覆盖了“发现人-了解人-建立联系”的完整链路而且每个模块的技术难度可控。我的建议是如果读者也想做类似的项目第一个版本就把这四个功能做扎实。什么视频动态、语音匹配、地图找人统统留给第二期再说。需求范围控制住了后端接口数量大约只需要15到20个一个人完全能在一到两周内写完。1.2 为什么后端选Flask而不是Django或Node这其实是小项目最常见的争论点。我的选择逻辑很简单Django功能全面但重自带Admin后台、ORM、Migration这些对于一个小程序后端来说超过一半用不上。而且Django的模板和配置体系需要额外学习成本学生项目往往比的是迭代速度不是工程复杂度。Flask的优势在于它像一块白板。你需要的功能自己装插件不需要的什么都不用管。配合Flask-SQLAlchemy做ORM、Flask-CORS处理跨域写社交平台后端的体验非常顺。项目结构自己控制目录清晰出了问题定位也快。Node生态的Express也是好选择但对Python熟的人来说Flask的代码可读性和调试体验更舒服。再加上后续要做推荐匹配的相似度计算Python生态里处理数据太方便了直接用列表推导和dict就能搞定没必要为了前后端同语言去引入Node。1.3 为什么前端用uniapp而不是原生小程序这是整个项目里我认为最正确的技术决策之一。原生微信小程序只能用WXML和WXSS语法跟Vue差异很大。如果只做小程序原生问题不大。但考虑到很多校园项目最终也要出一个H5版甚至安卓Appuniapp的价值就体现出来了。uniapp基于Vue语法写过Vue的人上手几乎没有门槛。编译到微信小程序时它帮你处理了大部分兼容性问题。最舒服的一点是页面路由、组件化、生命周期管理的方式跟Vue完全一致开发体验比原生小程序好太多。如果你实在纠结选型我建议评估两件事一是团队里有没有人会Vue二是项目有没有多端诉求。只要有其中一条成立uniapp就是更好的选择。不过要注意一个点uniapp编译到小程序后有一些API限制比如DOM操作基本不要想图片处理要用它的内置API。这些在开发前就要有心理预期不要拿小程序的逻辑直接套H5的经验。我用的是HBuilderX作为开发工具直接创建uniapp项目然后通过微信开发者工具进行预览调试。整体流程很顺畅后面会详细讲配置和打包。2. Flask后端核心接口设计与数据库模型2.1 项目目录结构怎么组织后端没有用复杂的微服务架构就是一个单体Flask应用。目录结构是实战中调优后的版本server/ ├── app.py # 应用入口注册蓝图 ├── config.py # 配置项数据库连接 ├── models/ │ ├── __init__.py │ ├── user.py # 用户模型 │ ├── post.py # 动态模型 │ ├── interaction.py # 点赞、评论、好友关系 │ └── tag.py # 兴趣标签模型 ├── api/ │ ├── __init__.py │ ├── auth.py # 登录授权接口 │ ├── user.py # 用户信息、资料编辑 │ ├── post.py # 动态发布、时间线 │ ├── match.py # 推荐匹配接口 │ └── chat.py # 聊天入口辅助 ├── utils/ │ ├── response.py # 统一返回格式 │ ├── auth_decorator.py # 登录态校验装饰器 │ └── similarity.py # 标签相似度算法 └── requirements.txt这种按功能模块拆分的结构好处是一目了然。哪个模块出了问题直接进对应文件夹不用在一个巨大的app.py里面翻上翻下。2.2 数据库模型设计数据库用的SQLite原因是轻量、零配置、适合本地部署。等用户量真正大了再去迁MySQL也不迟Flask-SQLAlchemy的ORM抽象让迁移成本很低。用户模型是我重点设计的。社交平台的用户表字段比普通系统多一些除了基础信息还必须有标签关联和交友状态from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() # 用户与标签的多对多关系表 user_tags db.Table( user_tags, db.Column(user_id, db.Integer, db.ForeignKey(user.id), primary_keyTrue), db.Column(tag_id, db.Integer, db.ForeignKey(tag.id), primary_keyTrue) ) class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) openid db.Column(db.String(64), uniqueTrue, nullableFalse) # 微信openid nickname db.Column(db.String(32), default) avatar_url db.Column(db.String(255), default) gender db.Column(db.String(8), defaultunknown) # male / female / unknown grade db.Column(db.String(16), default) # 年级 major db.Column(db.String(64), default) # 专业 bio db.Column(db.String(255), default) # 个性签名 campus db.Column(db.String(64), default) # 校区 created_at db.Column(db.DateTime, defaultdatetime.utcnow) # 兴趣标签 tags db.relationship(Tag, secondaryuser_tags, backrefusers) class Tag(db.Model): __tablename__ tag id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(16), uniqueTrue, nullableFalse) category db.Column(db.String(16), defaultinterest) # 标签分类 class Post(db.Model): __tablename__ post id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id), nullableFalse) content db.Column(db.Text, default) image_urls db.Column(db.Text, default) # 逗号分隔的图片地址 like_count db.Column(db.Integer, default0) comment_count db.Column(db.Integer, default0) created_at db.Column(db.DateTime, defaultdatetime.utcnow) user db.relationship(User, backrefposts) class LikeRecord(db.Model): __tablename__ like_record id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, nullableFalse) post_id db.Column(db.Integer, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) __table_args__ (db.UniqueConstraint(user_id, post_id, nameuk_user_post),) class Comment(db.Model): __tablename__ comment id db.Column(db.Integer, primary_keyTrue) post_id db.Column(db.Integer, nullableFalse) user_id db.Column(db.Integer, nullableFalse) content db.Column(db.String(255), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow)关键设计点有三个openid加了唯一约束。这是微信登录的天然主键每个用户在同一个小程序下的openid是唯一的用户标签表用多对多因为一个用户有多个标签一个标签也对应多个用户。匹配算法需要根据标签集合算相似度点赞表加了联合唯一约束防止同一用户对同一动态重复点赞2.3 微信登录接口的完整流程微信小程序登录的核心是code换openid。流程是小程序端调用 wx.login 拿到临时code小程序把code传给后端后端用code appid secret 调微信的 jscode2session 接口微信返回 openid 和 session_key后端拿openid查库有则登录无则注册后端实现import requests, hashlib from flask import Blueprint, request, jsonify from models import db, User from utils.auth_decorator import generate_token auth_bp Blueprint(auth, __name__, url_prefix/api/auth) auth_bp.route(/login, methods[POST]) def login(): data request.get_json() code data.get(code) if not code: return jsonify({code: 400, msg: 缺少code参数}), 400 # 调用微信接口换取openid appid 你的小程序AppID secret 你的小程序AppSecret resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: appid, secret: secret, js_code: code, grant_type: authorization_code }, timeout5 ) result resp.json() if errcode in result: return jsonify({code: 400, msg: 登录失败 result.get(errmsg, )}), 400 openid result[openid] # 查找或创建用户 user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid) db.session.add(user) db.session.commit() # 生成token返回给小程序端 token generate_token(user.id) return jsonify({ code: 0, data: { token: token, user_id: user.id, is_new: True # 实际可以记录一下是否为首次登录 } })这里有个重要细节jscode2session接口的访问域名不需要在小程序后台配置因为是后端服务器去请求微信接口不经过小程序前端。真正需要配置的是我们自己的后端域名。2.4 动态发布与时间线接口动态发布的接口逻辑比较简单但有一个坑是图片处理。小程序端用uni.chooseImage选图通过uni.uploadFile上传到服务器服务器需要处理图片的接收和存储。我花了一点时间把图片上传做成了独立接口import os, uuid from flask import Blueprint, request from werkzeug.utils import secure_filename upload_bp Blueprint(upload, __name__, url_prefix/api/upload) ALLOWED_EXTENSIONS {png, jpg, jpeg, gif, webp} upload_bp.route(/image, methods[POST]) def upload_image(): # 记得在config里配置UPLOAD_FOLDER的绝对路径 file request.files.get(file) if not file: return {code: 400, msg: 未收到文件} ext file.filename.rsplit(., 1)[-1].lower() if . in file.filename else if ext not in ALLOWED_EXTENSIONS: return {code: 400, msg: 不支持的图片格式} # 用uuid重命名避免文件名冲突 filename f{uuid.uuid4().hex}.{ext} save_path os.path.join(app.config[UPLOAD_FOLDER], filename) file.save(save_path) # 生成的URL需要是自己服务器的完整路径 url request.host_url.rstrip(/) /uploads/ filename return {code: 0, data: {url: url}}上传的图片用Nginx或Apache的静态目录托管不要直接让Flask代理静态文件因为并发量上来以后Flask处理静态文件效率不高。小程序端拿到图片URL后在Post模型里以逗号分隔的形式存多个图片地址。时间线接口这块我一开始走了弯路。第一版直接查所有Post然后按时间排序结果发现动态一多列表就很慢。优化方案是分页查询加可选的标签筛选post_bp.route(/timeline, methods[GET]) def timeline(): page int(request.args.get(page, 1)) per_page int(request.args.get(per_page, 10)) query Post.query.order_by(Post.created_at.desc()) pagination query.paginate(pagepage, per_pageper_page, error_outFalse) items [] for post in pagination.items: user post.user items.append({ post_id: post.id, content: post.content, image_urls: post.image_urls.split(,) if post.image_urls else [], like_count: post.like_count, comment_count: post.comment_count, created_at: post.created_at.strftime(%Y-%m-%d %H:%M:%S), author: { user_id: user.id, nickname: user.nickname, avatar_url: user.avatar_url, bio: user.bio } }) return {code: 0, data: {list: items, has_more: pagination.has_next}}page和per_page的分页参数是常规做法前端滚动到底部加载下一页这个后面会在uniapp部分配合说。3. uniapp微信小程序端从登录到动态流3.1 页面结构和tabBar设计uniapp的项目创建我用的是HBuilderX创建时选择“默认模板”然后勾选Vue 3版本。项目创建完成后pages.json就是路由和导航配置的核心文件。我的页面结构src/ ├── pages/ │ ├── index/index.vue # 动态广场首页 │ ├── match/match.vue # 匹配推荐 │ ├── publish/publish.vue # 发布动态 │ ├── message/message.vue # 消息入口 │ └── mine/mine.vue # 个人中心 ├── components/ │ ├── post-card.vue # 动态卡片组件 │ └── tag-selector.vue # 标签选择组件 ├── utils/ │ ├── request.js # 请求封装 │ └── auth.js # 登录态管理 ├── static/ │ └── logo.png ├── App.vue ├── main.js └── pages.jsonpages.json里重点配置tabBar这个是底部导航栏对应四个主页面{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 动态广场, enablePullDownRefresh: true } }, { path: pages/match/match, style: { navigationBarTitleText: 遇见 } }, { path: pages/publish/publish, style: { navigationBarTitleText: 发布动态 } }, { path: pages/message/message, style: { navigationBarTitleText: 消息 } }, { path: pages/mine/mine, style: { navigationBarTitleText: 我的 } } ], tabBar: { color: #999999, selectedColor: #3b7cff, borderStyle: black, backgroundColor: #ffffff, list: [ { pagePath: pages/index/index, text: 广场, iconPath: static/tab-home.png, selectedIconPath: static/tab-home-active.png }, { pagePath: pages/match/match, text: 遇见, iconPath: static/tab-match.png, selectedIconPath: static/tab-match-active.png }, { pagePath: pages/publish/publish, text: 发布, iconPath: static/tab-publish.png, selectedIconPath: static/tab-publish-active.png }, { pagePath: pages/message/message, text: 消息, iconPath: static/tab-message.png, selectedIconPath: static/tab-message-active.png }, { pagePath: pages/mine/mine, text: 我的, iconPath: static/tab-mine.png, selectedIconPath: static/tab-mine-active.png } ] } }tabBar的图标要用81x81像素左右的PNG而且必须小于40KB代码包有大小限制。我记得HBuilderX创建项目时自带的示例图标是可以直接用的但最好自己替换成项目风格的。3.2 请求封装与登录态管理这是小程序开发里最重要的基建。微信小程序的request API和axios的用法接近但有几个天生限制只能请求HTTPS接口开发模式可勾选不校验、需要配置合法域名、没有拦截器机制。所以必须自己做一层封装。我写了一个request.js// utils/request.js const BASE_URL http://localhost:5000/api; export function request(options) { return new Promise((resolve, reject) { const token uni.getStorageSync(token); uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { // 后端返回的格式是 { code: 0, data: ... } if (res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // token过期重新登录 uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }BASE_URL这里要注意如果要在微信开发者工具里调试http://localhost:5000/api是可以用的但要在开发者工具的“详情-本地设置”里勾选“不校验合法域名”。如果要在真机上预览就不能用localhost了要改成电脑的局域网IP比如http://192.168.1.100:5000/api。这个点我在第五章会专门展开因为太容易出问题了。登录态管理我用的是token方案。微信登录拿code换token后token存储在uni的Storage里。每次请求都带上token后端识别用户身份。3.3 登录流程uniapp端到后端在登录页或者App.vue的onLaunch阶段做自动登录// App.vue import { login } from ./utils/auth; export default { onLaunch: function() { // 先检查有没有token const token uni.getStorageSync(token); if (!token) { // 没登录去登录 this.handleLogin(); } }, methods: { async handleLogin() { // 微信登录获取code const loginRes await new Promise((resolve, reject) { uni.login({ provider: weixin, success: (res) resolve(res), fail: (err) reject(err) }); }); const code loginRes.code; // 把code发给后端 const data await request({ url: /auth/login, method: POST, data: { code } }); // 存储token和用户信息 uni.setStorageSync(token, data.token); uni.setStorageSync(userId, data.user_id); // 如果新用户跳到完善资料页 if (data.is_new) { uni.navigateTo({ url: /pages/profile/edit }); } } } }这里必须注意一个微信小程序的限制uni.login获取的code是一次性的而且有效期只有5分钟。如果后端请求微信接口超时这个code就作废了前端只能再调一次uni.login。所以后端的登录接口最好设置超时时间我上面写的5秒就是基于这个考虑。还有一点从2021年之后微信小程序已经不能用wx.getUserInfo直接弹出授权框获取用户头像和昵称了。微信改成了头像昵称填写能力也就是说用户主动填写或选择头像昵称。这个改动对学生项目影响很大——你不能首次登录就拿到用户昵称必须让用户自己填。我的做法是登录成功后判断is_new新用户跳转到一个资料编辑页用按钮触发uni.chooseAvatar让用户选择头像昵称用输入框让用户填。这个页面的实现template view classprofile-edit view classavatar-section image v-ifavatarUrl :srcavatarUrl classavatar / button classavatar-btn open-typechooseAvatar chooseavataronChooseAvatar 选择头像 /button /view input typenickname v-modelnickname placeholder请输入昵称 / button clicksaveProfile保存/button /view /template script export default { data() { return { avatarUrl: , nickname: , selectedTags: [] }; }, methods: { onChooseAvatar(e) { this.avatarUrl e.detail.avatarUrl; // 这里拿到的是临时路径要上传到自己的服务器 }, async saveProfile() { // 上传头像 - 提交资料 - 更新token里的用户信息 } } } /script头像上传之后拿到的临时路径要上传到自己的服务器然后后端返回一个永久URL。这个逻辑和动态图片上传是一样的调之前写的upload接口就行。3.4 动态流列表与分页加载动态广场是App的首页也是流量入口。列表页我用的是onShow生命周期去加载数据因为每次切到tab都要拿最新数据。配合onPullDownRefresh做下拉刷新。template view classfeed post-card v-foritem in feedList :keyitem.post_id :postitem likehandleLike / view v-ifloading classloading加载中.../view view v-ifnoMore classno-more没有更多了/view /view /template script import postCard from ../../components/post-card.vue; import { request } from ../../utils/request; export default { components: { postCard }, data() { return { feedList: [], page: 1, hasMore: true, loading: false, noMore: false }; }, onShow() { this.refresh(); }, onPullDownRefresh() { this.refresh(); }, onReachBottom() { if (this.hasMore) { this.loadMore(); } }, methods: { async refresh() { this.page 1; this.hasMore true; this.feedList []; await this.loadMore(); uni.stopPullDownRefresh(); }, async loadMore() { if (this.loading || !this.hasMore) return; this.loading true; try { const data await request({ url: /post/timeline?page${this.page}per_page10 }); this.feedList this.feedList.concat(data.list); this.hasMore data.has_more; this.page; this.noMore !data.has_more; } finally { this.loading false; } }, async handleLike(postId) { try { await request({ url: /post/like, method: POST, data: { post_id: postId } }); // 更新本地计数 const item this.feedList.find(p p.post_id postId); if (item) item.like_count; } catch (e) { // 错误处理 } } } }; /script分页加载的坑在于onReachBottom触发的时机。默认是在页面滚动到底部时触发但如果页面内容不满一屏它不会触发。所以我经常在onShow的时候先做一次loadMore确保第一屏数据足够。4. 兴趣匹配推荐相似度算法怎么落地4.1 标签体系设计推荐功能的底层是兴趣标签。大学生交友平台的标签体系不用搞得很复杂但要贴合场景。我设计了八个分类每个分类下几个常用标签分类标签示例运动篮球、羽毛球、跑步、健身、瑜伽文娱电影、音乐、读书、摄影、绘画游戏王者荣耀、原神、主机游戏、桌游学习考研、雅思、编程、创业、竞赛生活美食、旅行、猫奴、植物、穿搭二次元动漫、漫展、手办、Cos音乐民谣、说唱、摇滚、钢琴、吉他其他早起打卡、夜跑、志愿者、发呆标签不用太多每个用户最多选择8个。标签存数据库的Tag表用户选择和取消选择就是增删user_tags关联表的数据。用户资料编辑页面里有个tag-selector组件用可多选的标签网格template view classtag-selector view classtag-group v-forgroup in tagGroups :keygroup.category view classgroup-name{{ group.category }}/view view classtag-list view v-fortag in group.tags :keytag.id classtag-item :class{ active: selected.includes(tag.id) } clicktoggleTag(tag.id) {{ tag.name }} /view /view /view /view /template script export default { props: { selected: { type: Array, default: () [] } }, data() { return { tagGroups: [] }; }, created() { this.fetchTags(); }, methods: { async fetchTags() { const data await request({ url: /tag/list }); // 按category分组组装成tagGroups }, toggleTag(tagId) { const index this.selected.indexOf(tagId); if (index -1) { this.selected.splice(index, 1); } else if (this.selected.length 8) { this.selected.push(tagId); } else { uni.showToast({ title: 最多选择8个标签, icon: none }); } } } }; /script4.2 相似度算法的选择与实现匹配推荐的核心算法我对比过几种方案基于标签的余弦相似度把每个用户的标签集合看成向量计算夹角余弦Jaccard相似度交集大小除以并集大小基于人口的协同过滤需要用户历史行为数据冷启动问题严重大学生交友平台的用户量不会一开始就很大协同过滤的数据积累需要时间实用性不高。我最终选择了Jaccard相似度加距离惩罚。Jaccard相似度的公式是J(A, B) |A ∩ B| / |A ∪ B|两个用户共同标签越多相似度越高。它能直接反映兴趣重合程度解释性强而且计算成本低。假如用户A的标签是{篮球, 电影, 音乐}用户B的标签是{篮球, 音乐, 美食}交集是{篮球, 音乐}大小2并集是{篮球, 电影, 音乐, 美食}大小4J(A,B) 2/4 0.5。Python实现非常简洁def jaccard_similarity(set_a, set_b): if not set_a or not set_b: return 0.0 intersection len(set_a set_b) union len(set_a | set_b) return intersection / union if union 0 else 0.0如果两个用户标签集合有一方为空用户没选标签直接返回0这类用户不参与推荐。4.3 推荐接口的实现匹配推荐接口的逻辑如下获取当前用户的标签ID集合查全量用户排除自己逐个计算相似度按相似度降序排列取前N个附带返回共同标签列表方便前端展示“你们有3个共同爱好”我加了两个辅助条件来提升推荐质量同校区的用户排在前面相似度相同则按活跃度排。from utils.similarity import jaccard_similarity match_bp.route(/recommend, methods[GET]) def recommend(): # 假设从token解析当前用户 current_user get_current_user() limit int(request.args.get(limit, 20)) # 找出当前用户的标签集合 current_tag_ids set(tag.id for tag in current_user.tags) if not current_tag_ids: return {code: 0, data: {list: [], msg: 请先完善兴趣标签}} # 所有其他用户 all_users User.query.filter(User.id ! current_user.id).all() results [] for user in all_users: other_tag_ids set(tag.id for tag in user.tags) if not other_tag_ids: continue # 计算Jaccard相似度 sim jaccard_similarity(current_tag_ids, other_tag_ids) if sim 0: continue common_tags list(current_tag_ids other_tag_ids) # 有共同标签的最低保障分再多加一点校区加权 score sim * 100 if user.campus and user.campus current_user.campus: score 10 results.append({ user_id: user.id, nickname: user.nickname, avatar_url: user.avatar_url, bio: user.bio, score: round(score, 2), similarity: round(sim, 4), common_tags: common_tags, common_tag_count: len(common_tags), campus: user.campus }) # 先按score降序排 results.sort(keylambda x: x[score], reverseTrue) return {code: 0, data: {list: results[:limit]}}这个接口返回的数据可以直接支撑前端做卡片式推荐列表。前端每个卡片展示用户头像、昵称、共同标签用户可以直接点击进入个人主页。4.4 推荐质量的三个优化方向算法跑通之后我做了几轮优化这里分享几个实际踩过的点标签权重的缺失。Jaccard只看是否有共同标签但不同标签的权重应该是不同的。比如“考研”和“发呆”都是标签但前者交友匹配的价值完全不一样。目前的实现是等权后续如果要优化可以给高频普通标签降权给低频高价值标签加权。冷启动的兜底。新注册用户没选完标签之前推荐接口返回空列表体验不好。我的处理是如果当前用户或者对方没有标签就退化为按校区和活跃度推荐至少先把人露出来。无效信息的过滤。这个平台的特殊场景是有些用户注册后半年都不再打开。推荐结果里如果全是“僵尸号”很影响使用。我的过滤策略是筛选最近一周内有活跃记录的用户优先靠前超过30天无活跃的不进入推荐主列表。5. 本地联调与微信开发者工具的那些坑5.1 最隐蔽的坑localhost在小程序端失效这是新人最容易踩的坑。在微信开发者工具里调试如果后端跑在http://localhost:5000小程序端uni.request请求这个地址在开发者工具里可能正常但一用手机扫码预览就必定失败。原因是手机访问的localhost是手机自己不是你电脑。解决方案不要用localhost改用电脑的局域网IP。先查电脑IP# Windows ipconfig # macOS ifconfig | grep inet比如电脑IP是192.168.1.100那BASE_URL就要改成http://192.168.1.100:5000/api。同时确保手机和电脑在同一个WiFi下而且电脑防火墙允许Flask端口被局域网访问。Flask默认的host是127.0.0.1只能本机访问。要监听所有网卡必须这样启动python app.py --host0.0.0.0 --port5000或者直接在app.run里写死if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这样局域网内的手机才能通过IP访问到你的后端。5.2 微信开发者工具的域名校验微信小程序的合法域名限制是正式上线后request的url必须在微信公众平台后台配置为HTTPS合法域名。但开发阶段可以绕过在微信开发者工具中点击右上角的“详情” - “本地设置” - 勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。这个开关只影响开发者工具和预览模式不影响正式体验版。预览模式下真机调试会自动绕过校验但如果你把预览二维码发给别人别人的手机上能不能正常访问取决于对方是不是开发者账号。实际上微信对体验版也有单独的域名校验策略正式体验前必须配置好合法域名。5.3 CORS跨域问题如果在开发阶段直接用浏览器打开H5版调试Flask后端会遇到跨域问题。小程序端不存在跨域限制但H5端有。处理方式是给Flask配置CORSfrom flask_cors import CORS app Flask(__name__) CORS(app, resources{r/api/*: {origins: *}})这里有个取舍origins*方便开发但生产环境如果不需要H5端访问建议关掉或者收紧为具体域名避免别人直接调你的接口。5.4 顶部导航栏与自定义导航的适配小程序页面默认有系统导航栏但有时候我们想自定义导航栏来放搜索框或自定义标题。这时需要在pages.json的style里设置{ path: pages/index/index, style: { navigationStyle: custom } }自定义导航栏后需要自己处理状态栏高度和胶囊按钮的位置。微信小程序的胶囊按钮右上角的三个点加圆形按钮高度是固定的但不同机型顶部状态栏高度不同。获取状态栏高度的方法const systemInfo uni.getSystemInfoSync(); console.log(systemInfo.statusBarHeight); // 状态栏高度单位px胶囊按钮的位置信息可以通过uni.getMenuButtonBoundingClientRect()获取包括它的top、bottom、height。自定义导航栏高度一般建议是导航栏高度 (胶囊按钮top - 状态栏高度) * 2 胶囊按钮height这是一个非常常见但没人告诉你公式的点。我在适配iPhone 14 Pro和普通安卓机的时候就靠这个公式调平了导航栏位置。5.5 真机调试的几个隐藏问题真机预览时的几个问题值得单独列出来uni.request的超时设置默认60秒建议设为10到15秒。网络差的时候用户不能一直等着图片域名真机上加载图片的URL也要是HTTPS域名开发阶段http图片在真机上可能被拦截缓存问题修改了代码之后真机预览可能会出现旧缓存。微信开发者工具的“清缓存”功能要经常用蓝牙和定位权限这些这里用不到但社交平台涉及选择相册图片的权限需要在app.json里声明6. HBuilderX打包配置与上线前检查清单6.1 manifest.json的核心配置uniapp项目的配置文件分两块mainfest.json控制应用级配置pages.json控制页面级配置。上线前重点检查mainfest.json的小程序部分{ mp-weixin: { appid: 你的微信小程序AppID, setting: { urlCheck: true, es6: true, postcss: true, minified: true }, usingComponents: true, permission: {} } }appid必须是你自己在微信公众平台注册的小程序AppID不能用测试号。urlCheck改成true之后项目里所有请求的域名必须在小程序后台完成配置否则会报“url not in domain list”。打包时HBuilderX提供几个选项传统打包、云打包。我本地没有安装微信开发者工具时用云打包但云打包需要注册DCloud账号。打出来的包是一个压缩文件里面是微信小程序代码要用微信开发者工具导入才能预览。注意一点uniapp的代码包大小上限是2MB。如果你的图片资源、组件库太大编译时会警告。解决办法是压缩图片、按需引入组件或者把静态资源放到CDN引用绝对路径。6.2 微信公众平台的服务器域名配置这是上线前的必经步骤也是好多第一次做小程序的人卡住的地方。登录微信公众平台 - 开发管理 - 开发设置 - 服务器域名。需要配置三类request合法域名你的后端API域名必须是HTTPSuploadFile合法域名图片上传接口域名downloadFile合法域名下载资源、加载图片的域名如果有独立图片服务器的话假如你的后端是https://api.example.com那配置如下request合法域名: https://api.example.com uploadFile合法域名: https://api.example.com downloadFile合法域名: https://api.example.com配置生效时间很快但切记域名必须有ICP备案且支持HTTPS。微信审核时会校验HTTPS证书的有效性。6.3 上线前必须检查的功能清单我把上线前的检查项整理成一个清单逐项核对过才发版用户协议和隐私政策小程序的“设置”里需要展示社交类目强制要求。最简单的方式是做一个静态页面链接放在“我的”页面里登录流程新用户能注册老用户能登录token过期能自动重新登录头像昵称确认新用户能正常触发选择头像和填写昵称动态发布文字、图片、长短内容都要测试发布成功后能出现在时间线列表里点赞评论点赞数能正确累加不能重复点赞评论能正常展示匹配推荐新用户没选标签时要有兜底策略选完标签后推荐结果合理会话入口点击“联系TA”能正确跳转分享功能小程序页面支持分享卡片分享出去的页面访问正常审核类目的选择也值得提前想清楚。微信公众平台的“社交-陌生人交友”类目需要额外资质比如增值电信业务经营许可证学生个人开发者很难提供。我当时通过调整产品描述用“校园综合性服务工具”方向提交囊括了动态广场和匹配功能审核通过。这里每个项目的实际情况不同我不是建议你去钻空子而是要提前研究类目要求避免做好功能结果审核不过。6.4 云端部署的简单方案本地部署没问题后上线就需要一台服务器。我用的方案是服务器一台2核4G的云服务器Linux系统部署方式Gunicorn Nginx反向代理数据库SQLite直接放在服务器上小流量完全够HTTPS用平台提供的免费证书部署到NginxGunicorn启动Flask的基本命令gunicorn -w 2 -b 127.0.0.1:5000 app:app --timeout 60Nginx配置一段反向代理把外部443端口转发到内部5000端口server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } location /uploads/ { alias /var/www/uploads/; } }有一个细节是Gunicorn的worker数量不是越多越好。2核4G的机器开2个worker就能支撑很大的并发量。开太多反而会因为内存不够导致频繁重启。写在最后的实操体会项目做完一轮后我的最大感受是大学生社交平台的技术实现其实不复杂复杂度反而在“人”这边。用户标签选不好推荐算法再精巧也没用用户不发布动态广场就永远是空的。所以如果你也打算做类似项目我强烈建议在开发的同时就让种子用户进来用用真实数据反推算法和交互的调整方向。还有一个小技巧值得分享在标签选择页面加上一个“智能推荐标签”按钮根据用户的年级和专业预填一些标签能显著提高用户完成资料编辑的意愿。我实测下来预填了标签的完成率比手动从零开始选高了40%以上。这就是整个项目从技术选型到落地上线的完整过程。Python加Flask做后端uniapp做小程序前端配合一个轻量级SQLite数据库一个人也能撑起一个完整的校园社交平台。如果你正在规划类似项目希望这篇内容能帮你少走几步弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →