ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flask + UniApp 打造同城钓鱼社交小程序:架构设计与实战踩坑全记录

Flask + UniApp 打造同城钓鱼社交小程序:架构设计与实战踩坑全记录 做了个钓鱼同好者的小程序后端用 Python Flask 搭接口前端用 UniApp 编译成微信小程序前后端加起来差不多花了三周的业余时间。这个项目做出来之后身边几个钓友群都在问能不能开放注册说实话确实解决了同城钓友找伴、交流钓点这类实名社交的刚需。这篇就把整个设计和开发过程从头到尾梳理一遍包括架构怎么选型、数据库怎么设计、接口怎么规划、UniApp 怎么处理微信小程序的兼容问题以及那些只有真踩过坑才懂的细节希望对想做类似同城社交项目的朋友有点参考价值。1. 项目概述与整体设计思路1.1 同城钓鱼社交的核心需求拆解做同城钓鱼论坛不能简单套一个普通论坛的壳。钓鱼这个圈子有自己的特殊性钓友最关心的是哪里有好的钓点、最近鱼情如何、什么时候适合出钓、谁能一起拼车去水库或者野河边。这些需求本质上都是基于地理位置的信息交换和普通话题讨论不一样。所以这个项目虽然名字叫“论坛交流”但核心功能其实拆成了三块内容社区用户发帖分享钓获、晒鱼获、交流钓法。这一层是内容沉淀解决“钓友感兴趣什么”的问题。同城维度所有帖子、钓点、约钓信息都围绕城市和具体位置展开。用户打开小程序第一件事就是选择城市然后看到的是同城钓友动态。“同城”是社交关系破冰的第一层过滤器。线下约钓真正让线上转为线下的桥梁。发一个约钓帖写明时间、地点、人数同城的钓友看到后可以申请加入这是一种关系链的延伸。这三个层次是递进关系普通论坛只做到了第一层很多死掉的垂直社区连第一层都没做好。我在设计之初就定了这个基调内容只是媒介同城是信任基础线下面约是终极转化。所以后端接口设计上所有核心查询都绕不开地理位置字段。1.2 技术选型Flask UniApp 的方案考量技术选型当时也纠结过。先说后端为什么是 Flask 而不是 Django 或者 Node.js我的考虑很简单这个项目是典型的轻社交应用数据模型不算复杂核心就用户、帖子、评论、点赞、关注这些表Django 自带的那套后台管理和 ORM 骨架反而有点重。Flask 够轻够灵活配合 SQLAlchemy 操作数据库配合 JWT 做鉴权半天就能把项目骨架搭起来。纯 API 服务也不需要模板渲染Flask 把全部 HTTP 逻辑交给前端自己只负责 JSON 往来这种边界感非常清晰。再一个原因是 Python 生态里的爬虫和数据处理库很好用后期如果想加钓点水质数据抓取、鱼情数据采集这些功能用 Python 写会顺很多。这算是给项目未来留了个口子。前端选 UniApp 而不是原生微信小程序这个问题我在不少群里被问过。UniApp 的底层是 Vue写的是一套代码同时能编译成微信小程序、H5、App。这个项目虽然首发目标只上微信小程序但同城社交这种产品未来大概率会有 App 端的需求——钓友在野外的信号环境很复杂小程序在弱网下的表现并不理想很多钓友用的是安卓机装一个轻量 App 反而是常态。UniApp 这套方案相当于把未来要写的安卓/iOS 端成本提前扣除了很少一部分。更重要的是 UniApp 对 Vue 开发者的友好度极高。Vue 的双向绑定、组件化、计算属性这些思路搬到小程序开发上比原生 WXML JS 的自造一套更顺手。虽然底层编译后会在性能上有轻微损耗但做内容型社区这种非强交互场景感知几乎为零。1.3 项目整体架构与开发流程规划整个系统的架构非常清晰三层结构展示层UniApp 编写页面编译成微信小程序。服务层Flask 提供 RESTful API负责用户认证、业务逻辑、数据校验。数据库层MySQL 存储结构化数据Redis 存缓存和临时状态比如验证码、点赞状态。开发流程上我没有按传统先后端后前端的方式推进。后端先写接口文档然后前后端并行开发。Flask 这边用 Postman 测接口UniApp 这边用 Mock 数据先跑页面。这样做最大的好处是压缩了整体周期因为前后端只靠接口文档对齐不需要等对方代码写完才能开工。项目的开发顺序按照模块拆用户系统 → 帖子系统 → 评论点赞 → 同城发现 → 个人中心。每一个模块走通一个完整闭环而不是先把后端全部写完再碰前端。这算是我做这种小项目的一个经验按纵向切片推进每个切片都能看得到东西心里有底也方便随时调头改需求。2. Flask 后端架构设计与数据库建模2.1 后端项目结构与 Flask 工厂模式后端没有用单文件跑到底那种极简方式。真实项目哪怕再小代码一多之后单文件就是灾难。我按功能模块把项目拆成了蓝本目录结构flask_fishing/ ├── app/ │ ├── __init__.py # 应用工厂 扩展初始化 │ ├── config.py # 配置管理 │ ├── models/ # SQLAlchemy 数据模型 │ │ ├── user.py │ │ ├── post.py │ │ └── social.py │ ├── api/ │ │ ├── auth.py # 登录注册 │ │ ├── posts.py # 帖子相关接口 │ │ ├── comments.py # 评论回复 │ │ ├── location.py # 钓点相关 │ │ └── users.py # 用户信息 │ ├── utils/ │ │ ├── jwt_auth.py # Token 装饰器 │ │ ├── response.py # 统一返回格式 │ │ └── geo.py # 距离计算 ├── migrations/ # 数据库迁移脚本 ├── run.py # 启动入口 └── requirements.txt应用工厂模式是 Flask 官方文档推荐的做法也是我后来再也回不去单文件的原因。工厂函数在app/__init__.py里动态创建 app 实例配置、数据库、JWT 这些扩展都在工厂里完成绑定。这样做的好处是需要多少个配置环境就创建多少个 app 实例本地开发一套配置生产部署另一套配置测试又一套配置互不干扰。# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS from flask_migrate import Migrate db SQLAlchemy() migrate Migrate() def create_app(config_namedefault): app Flask(__name__) app.config.from_object(config[config_name]) db.init_app(app) migrate.init_app(app, db) CORS(app, supports_credentialsTrue) # 注册蓝图 from app.api.auth import auth_bp from app.api.posts import posts_bp from app.api.comments import comments_bp from app.api.location import location_bp from app.api.users import users_bp app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(posts_bp, url_prefix/api/posts) app.register_blueprint(comments_bp, url_prefix/api/comments) app.register_blueprint(location_bp, url_prefix/api/location) app.register_blueprint(users_bp, url_prefix/api/users) return appCORS 在开发阶段一定要配好。我用微信开发者工具调试的时候如果配置不对会出现请求直接发送失败的情况。虽然小程序本身不受浏览器同源策略限制但热词里提到的“uniapp封装H5”场景以及后期如果要把同一套 UniApp 代码编译成 H5 站点CORS 配置不到位就是硬伤。所以哪怕现阶段只要小程序端我还是提前配好了supports_credentialsTrue避免以后踩坑。2.2 数据库模型设计用户、帖子、评论与关注关系数据库设计是整个社交系统的地基这块做得不扎实后面写接口会非常痛苦。我设计的核心表有六张用户表、帖子表、评论表、点赞表、关注表、钓点表。用户表除了基本字段额外增加了两个和同城相关的字段city_code和location_desc。city_code用于同城筛选location_desc是用户填写的个人常驻区域描述。为什么不直接存经纬度因为钓友个人常驻位置属于隐私用户不太愿意精确暴露自己的住址存城市码加模糊描述既满足同城匹配需求又在隐私和功能之间取了平衡。帖子表是社区内容的核心这里的字段设计会直接决定后期查询效率post: id, user_id, title, content, category(鱼种分类), address_desc, lat, lng, images(JSON数组), video_url, like_count, comment_count, view_count, status, created_at, updated_atlat和lng是发帖时定位的经纬度这两个字段被location接口用到。images字段我用 JSON 数组存图片路径列表这是 NoSQL 思维和关系型数据库结合的常见做法——MySQL 5.7 以上原生支持 JSON 类型比单独建一张图片表要省事得多。查询的时候直接取值不需要额外的表关联。评论表和点赞表走的是标准社交设计。点赞表有个细节加一个canceled字段软删除而不是直接删记录。用户点赞-取消-再点赞时如果每次都删除再插入数据库会产生大量碎片记录用软删除只需要更新一个字段的状态性能好很多。关注关系表存的是follower_id和followed_id查询粉丝数、关注数、是否关注这几个指标都是非常高频的操作。我额外建立了一个联合唯一索引(follower_id, followed_id)从数据库层面防住重复关注。2.3 JWT 鉴权与用户会话管理微信小程序的登录流程和普通 Web 登录完全不一样。你不能在小程序里让用户输入用户名密码正确姿势是前端调用wx.login()拿到 code然后把这个 code 传给后端后端拿 code 去向微信服务器换 openid 和 session_key。这个流程在后端的实现是这样的# app/api/auth.py import requests def code2session(code): url https://api.weixin.qq.com/sns/jscode2session params { appid: current_app.config[WX_APPID], secret: current_app.config[WX_SECRET], js_code: code, grant_type: authorization_code } resp requests.get(url, paramsparams).json() return resp auth_bp.route(/wx_login, methods[POST]) def wx_login(): data request.get_json() code data.get(code) user_info data.get(userInfo) # 通过 code 换取 openid sess code2session(code) if errcode in sess: return jsonify(code400, msg微信登录失败) openid sess[openid] # 根据 openid 查用户没有就自动注册 user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid, nicknameuser_info.get(nickName, 钓友)) db.session.add(user) db.session.commit() # 签发 JWT token jwt.encode( {user_id: user.id, exp: datetime.utcnow() timedelta(days30)}, current_app.config[SECRET_KEY], algorithmHS256 ) return jsonify(code200, data{token: token, user: user.to_dict()})JWT 令牌签发后后续所有接口都靠请求头里的Authorization: Bearer token来识别身份。我在utils/jwt_auth.py里写了一个装饰器login_required所有需要登录状态的接口直接加上这个装饰器def login_required(f): wraps(f) def decorated(*args, **kwargs): auth_header request.headers.get(Authorization) if not auth_header or not auth_header.startswith(Bearer ): return jsonify(code401, msg未登录) try: token auth_header.split( )[1] payload jwt.decode(token, current_app.config[SECRET_KEY], algorithms[HS256]) current_user User.query.get(payload[user_id]) if not current_user: return jsonify(code401, msg用户不存在) g.current_user current_user except jwt.ExpiredSignatureError: return jsonify(code401, msg登录已过期) except Exception: return jsonify(code401, msg无效的令牌) return f(*args, **kwargs) return decorated有个坑必须提醒JWT 的有效期策略。如果设置太短用户在小程序里挂着挂着突然要重新登录体验很差设置太长又有安全隐患。我这边用 30 天同时在前端用 UniApp 的uni.setStorageSync把 token 存在本地每次启动时检查 token 是否过期。真正生产环境还应该加入 refresh token 机制但对这种体量的项目来说单 token 加合理过期时间够用了。2.4 同城钓点发现与距离计算同城功能是项目的主卖点之一所以在后端专门写了geo.py做距离计算和范围筛选。地理坐标距离计算有个入门算法叫 Haversine 公式用来算两个经纬度点之间的球面距离。精度能满足日常使用而且计算量非常小。# app/utils/geo.py import math def haversine_distance(lat1, lng1, lat2, lng2): R 6371 # 地球半径单位公里 dlat math.radians(lat2 - lat1) dlng math.radians(lng2 - lng1) a math.sin(dlat/2)**2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlng/2)**2 c 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return round(R * c, 2)同城筛选的思路是前端拿到用户当前定位通过wx.getLocation把经纬度传给后端的/api/location/nearby接口后端返回半径 20 公里内的钓点和相关帖子。我实际测过这个方案的性能MySQL 里如果有 1 万条带经纬度坐标的帖子记录直接用 Haversine 公式遍历计算距离响应时间在 60ms 左右完全够用。但如果数据量增长到 10 万级别这个方案就扛不住了需要引入空间索引或者用 Geohash 先粗筛再做精确计算。项目初期不必过度设计但代码里的geo.py留了替换空间后期升级不影响接口契约。3. UniApp 前端开发与微信小程序适配3.1 UniApp 项目初始化与目录结构规划UniApp 官方提供了 CLI 脚手架和 HBuilderX 两种创建方式。我更推荐 CLI 方式npx dcloudio/uvm创建项目配合 Vue 3 Vite整个开发体验和写普通 Vue 项目几乎没区别。HBuilderX 虽然内置了编译和运行能力但编辑器体验弱于 VS CodeCLI 方式还能用自己熟悉的代码编辑器。项目结构上按业务模块划分src/ ├── api/ # 接口请求统一封装 │ ├── request.js # 请求核心逻辑 │ ├── auth.js │ ├── post.js │ └── location.js ├── pages/ │ ├── index/ # 首页-同城动态 │ ├── discover/ # 发现-钓点地图 │ ├── publish/ # 发布动态 │ ├── message/ # 消息通知 │ └── mine/ # 个人中心 ├── components/ # 公共组件 ├── static/ # 静态资源 ├── store/ # Pinia 状态管理 ├── manifest.json # 应用配置 ├── pages.json # 页面路由配置 └── main.jsmanifest.json里的微信小程序配置是重点。AppID 填自己注册的小程序 AppIDmp-weixin节点下配置requiredPrivateInfos——如果你用了wx.getLocation必须在manifest.json里面声明requiredPrivateInfos: [getLocation]不然真机调试直接报错这是微信官方这两年收紧隐私权限后的硬性要求老项目迁移时特别容易遗漏。3.2 请求封装与拦截器设计小程序开发中网络请求封装得好不好直接决定整个项目的开发效率。我在api/request.js里封装了一个统一的请求实例核心逻辑包括基础 URL 配置、token 自动注入、响应拦截、错误统一处理。// src/api/request.js import { useUserStore } from /store/user const BASE_URL https://api.yourdomain.com/api export function request(options) { return new Promise((resolve, reject) { const userStore useUserStore() uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: userStore.token ? Bearer ${userStore.token} : , ...options.header }, success: (res) { if (res.statusCode 401) { // token 失效跳转登录 uni.navigateTo({ url: /pages/login/login }) reject(new Error(登录已过期)) return } if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { uni.showToast({ title: 网络错误请检查网络, icon: none }) reject(err) } }) }) }在热词里有“微信小程序请求封装”和“uniapp 封装h5如何指向2个域名”这两个都是我实际遇到的问题。H5 和微信小程序的环境差异在这里暴露得很明显小程序端请求走的是uni.request不需要考虑跨域H5 端因为有浏览器同源策略限制需要后端配好 CORS而且 H5 的本地调试域名比如 localhost:8080和生产域名比如 api.yourdomain.com是两个不同的源。我的方案是在request.js里根据编译平台动态切换 BASE_URL// #ifdef H5 const BASE_URL http://localhost:5000/api // 本地调试 // #endif // #ifndef H5 const BASE_URL https://api.yourdomain.com/api // 小程序和App走正式环境 // #endifUniApp 的条件编译指令#ifdef和#ifndef是解决跨端差异的核心工具这个必须学会。它是在编译阶段直接裁剪代码不是运行时的 devicn 判断也就是说H5 端构建的时候小程序端的代码块根本不会被打进包里去从根上杜绝了兼容问题。3.3 页面布局与微信小程序导航适配微信小程序的顶部导航栏有个老大难问题不同机型的状态栏高度和胶囊按钮位置不一样。如果你做自定义导航栏必须手动计算状态栏高度。搜索热词里就有“微信小程序顶部导航栏高度”。用 UniApp 处理这个问题的推荐方案是pages.json里配置navigationStyle: custom开启自定义导航然后在页面里用 CSS 变量动态适配// 在 App.vue 的 onLaunch 里 uni.getSystemInfo({ success: (res) { // 状态栏高度 const statusBarHeight res.statusBarHeight // 胶囊按钮位置信息 const menuButton uni.getMenuButtonBoundingClientRect() // 导航栏高度 胶囊按钮高度 上下间距 const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height // 存到全局 uni.$statusBarHeight statusBarHeight uni.$navBarHeight navBarHeight } })这个算出来以后每个自定义导航栏的页面直接在样式中取这个值基本上不用考虑适配问题。我当初做的时候没算胶囊按钮的下边缘结果在 iPhone 上正常一换安卓机导航栏就顶到状态栏下面去了。这里有个经验适配一定要用真机测开发者工具的模拟器器和真机差异巨大。3.4 微信小程序 login 登录流程与自定义分享微信小程序的登录前端的主要工作是拿到wx.login的 code 并把它传给后端。代码很简洁// src/api/auth.js export function wxLogin(userInfo) { return new Promise((resolve, reject) { uni.login({ provider: weixin, success: async (loginRes) { const { code } loginRes const res await request({ url: /auth/wx_login, method: POST, data: { code, userInfo } }) resolve(res) }, fail: (err) reject(err) }) }) }自定义分享也是一个常见需求。默认分享只能分享整个小程序首页体验不好。我希望用户能分享一篇具体的钓鱼好贴给朋友点开直接进入帖子详情页。实现方式是在帖子详情页的onShareAppMessage生命周期里配置onShareAppMessage() { const post this.currentPost return { title: 【${post.cityName}】${post.title}, path: /pages/detail/detail?id${post.id}, imageUrl: post.coverImage } }这里有个细节分享图片imageUrl必须是小程序白名单里的域名地址或者直接用wx.cloud的存储地址否则分享卡片渲染不出来。我在这里卡了半天最后检查网络图片的域名白名单配置才解决。3.5 发布页面与图片上传发帖页是用户创作的核心入口也是技术难点集中地文字编辑、图片选择、定位获取、发布逻辑。我用 UniApp 的uni.chooseMedia来实现图片选择——注意微信小程序端早期用的是uni.chooseImage但现在官方推荐chooseMedia因为它支持视频选择参数更灵活。图片上传走了直传链路前端把图片传给后端接口后端把图片存到服务器指定目录返回图片 URL然后前端拿着 URL 去调用发帖接口。这种方案在小体量项目里够用但要注意后端需要做上传权限校验、文件类型白名单和大小限制。# app/api/posts.py 上传接口片段 ALLOWED_EXTENSIONS {png, jpg, jpeg, gif, webp} posts_bp.route(/upload, methods[POST]) login_required def upload_image(): file request.files.get(file) if not file: return jsonify(code400, msg没有上传文件) ext file.filename.rsplit(., 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: return jsonify(code400, msg不支持的图片格式) if file.content_length 5 * 1024 * 1024: return jsonify(code400, msg图片大小不能超过5MB) filename f{uuid.uuid4().hex}.{ext} file.save(os.path.join(UPLOAD_FOLDER, filename)) url f/uploads/{filename} return jsonify(code200, data{url: url})发布界面里还加了定位授权开关。不授权的用户也可以发帖只是没有地理信息同城聚类的功能对这类帖子不生效。这个设计是为了照顾一部分不喜欢分享位置的用户也让首次授权失败时不会直接中断发布流程。4. 核心功能模块的详细实现4.1 帖子动态流与分页加载首页同城动态流的实现思路是后端接口/api/posts接收city_code、page、page_size、sort_by四个参数返回帖子列表和总页数。前端用触底加载分页的方式而不是一次性全量渲染避免数据量大时页面卡死。分页查询的时间复杂度优化是关键。如果直接SELECT * FROM posts WHERE city_code ? ORDER BY created_at DESC LIMIT ? OFFSET ?在数据量大后 OFFSET 会越来越慢。我用了游标分页的思路替代传统分页posts_bp.route(, methods[GET]) def get_posts(): city_code request.args.get(city_code) before_id request.args.get(before_id, typeint) limit request.args.get(limit, default10, typeint) query Post.query.filter_by(status1) if city_code: query query.filter_by(city_codecity_code) if before_id: query query.filter(Post.id before_id) posts query.order_by(Post.id.desc()).limit(limit).all() return jsonify(code200, data[p.to_dict() for p in posts])前端下拉加载的时候把当前列表最后一条帖子的 ID 作为before_id传给后端就能拿到更早的数据。这种游标方式不会因为新增帖子导致已加载数据重复或者跳页体验比页码分页稳很多。这个设计在真实社交产品里很常用值得记下来。4.2 钓鱼地点标记与天气联动钓点的功能我做了两层第一层是用户分享的标记第二层是天气联动提示。钓点标记在发布帖子时附带位置信息后端把经纬度存下来之后通过逆地址解析调用高德地图 API自动补充城市和区域名称。这个做法省去用户手动填位置的麻烦。天气联动是从“钓鱼前要看天气”这个习惯出发做的。去野钓尤其是夏季和冬季气压、气温、风向对鱼口影响非常明显。我调用了一个免费的天气接口在钓点详情页展示当前实时天气和气压值还做了一个非常粗的方案性的鱼情指数——根据气压变化趋势给出“适合出钓”或“不建议出钓”的建议。这个功能技术不复杂但用户反馈非常好。很多钓友说打开小程序先看天气再决定去哪里。这其实说明了一个产品规律同城社交工具如果能切中用户实实在在的场景痛点哪怕功能很轻粘性也会很高。4.3 评论、点赞与消息通知评论支持二级而不是无限嵌套原因有两层第一无限嵌套对移动端 UI 不友好用户在小屏幕上一层层折叠展开操作成本极高第二技术上每一次展开都要额外请求接口数据库递归查询也会增加压力。我做了第一层“帖子 → 顶级评论”回复别人的评论时通过parent_id来关联展示层统一渲染成缩进的样式。这种简化在移动端社交产品里是常见取舍。点赞功能在数据库层面做了幂等处理。用 Redis 的SADD命令和SISMEMBER命令来处理点赞和判断是否点赞过这样即使前端重复点击按钮后端也不会出现重复点赞的数据脏问题。但如果项目不上 RedisMySQL 的唯一索引也能做到同样效果——前面提过(user_id, post_id)的唯一索引配合软删除字段canceled点踩后取消再点的逻辑也很顺畅。消息通知用的是类似“铃铛”的机制评论、点赞、关注、约钓申请这些行为都会往消息表里插一条数据。用户进入消息页后端按时间倒序返回未读消息列表。这个功能在没有 WebSocket 的情况下用的是轮询方案——前端每隔 15 秒拉一次新消息。小程序活跃度不高轮询不会对服务器造成可见压力。真正需要实时聊天功能时再考虑 WebSocket 升级。5. 开发中遇到的坑与排查记录5.1 Flask 后端的经典坑跨域配置。开发阶段用 H5 端调试时Flask 后端必须允许跨域。我一开始只配了CORS(app)全放开后来发现带Authorization: Bearer token的请求还是被浏览器拦截检查之后才知道是预检请求 OPTIONS 没处理。Flask-CORS 默认就是支持这个的但如果自己手写了装饰器容易漏。直接推荐用 Flask-CORS 扩展并配好supports_credentialsTrue省心。SQLAlchemy 查询到字典的序列化。很多新手会直接return jsonify(data, defaultstr)结果 datetime 字段直接报错或转成奇怪的格式。我统一的写法是在每个 model 类里实现一个to_dict()方法把字段手动处理一遍。虽然有点啰嗦但胜在可控。生产环境还可以考虑接入 marshmallow 这类序列化库代码会更优雅。本地时间原因引起的时间差。created_at存的时候用的是 UTC 时间显示的时候要在前端转成北京时间。如果你直接用datetime.utcnow()存库前端取回的时间会差 8 个小时。比较踏实的方案是用datetime.now()配合 MySQL 的timestamp类型自动转换时区或者在前端统一用时间戳做转换。5.2 UniApp 跨端与微信小程序兼容坑UniApp 虽然号称一套代码多端运行但实测下来真的有太多“暴露平台差异”的地方了。我遇到的第一个大坑是CSS 支持差异。开发时写的display: flex在 H5 表现正常但真机小程序里偶尔崩布局排查半天发现是gap属性在小程序 WebView 内核里不支持。解决方案用 margin 替代或者自己封装 flex 通用类。第二个坑是输入框在键盘弹出时的表现。!-- textarea 在 H5 里面正常在小程序里被原生键盘顶上去布局错乱 --。淘宝、小红书这类工具都会遇到这个状况。我用adjust-position属性和bindkeyboardheightchange来动态调整页面底部安全区距离最终解决了输入框被遮挡的问题。第三个坑是打包体积。小程序对主包有 2MB 限制图片、组件一多就容易超。我的处理方案尽量用分包加载把发布、消息这种低频页面放进分包图片统一走 CDN 而不是本地静态资源第三方库按需引入别为了用一两个函数把整个库装进包。5.3 小程序发布审核遇到的那些事微信小程序审核是绕不开的关。我这个项目遇到过两个比较典型的问题第一社交类目资质问题。小程序如果涉及用户生成内容UGC功能微信要求必须有社交类目的资质。个人开发者账号基本没有申请这类目权限所以发布之前一定要搞清楚自己的类目要求。我当时选择了“生活服务 运动 休闲服务”这个类目把 UGC 功能作为附加能力来申报沟通了几次之后过了审核。第二内容安全接口调用。微信要求 UGC 平台必须调用内容安全接口msgSecCheck和mediaCheckAsync对用户发布的文字和图片进行校验。这是合规要求也是自己保护平台避免垃圾信息的措施。我在 Flask 后端写了个security_check.py在发帖和评论接口内部调用微信官方的内容安全检测接口检测通过才允许写入数据库否则返回违规提示。5.4 弱网环境的接口优化钓鱼人常在郊区、河边、水库这些地方信号质量飘忽不定这是这个项目的特殊场景约束。所以接口性能和缓存策略比普通城市应用更要重视。我做的主要工作有三块所有列表页接口加接口级缓存回复数据 30 秒内有效。静态资源全部走 CDN尤其图片加载速度提升非常明显。前端在request.js里对相同请求做了节流合并防止用户快速滑动时频繁触发重复请求。实测下来野外 4G 环境下冷启动页面到首屏数据渲染的时间从 2.5 秒降到了 1.2 秒左右用户体验改善很大。5.5 常见问题速查表问题现象可能原因解决方案小程序拿不到定位权限manifest.json 缺少requiredPrivateInfos配置在 manifest 的 mp-weixin 节点添加定位权限声明图片上传失败 401上传接口没有带 token检查uni.uploadFile是否手动加入了Authorization头H5 请求接口报跨域错误Flask 后端 CORS 配置不全用 Flask-CORS 并设supports_credentialsTrue列表触底加载重复数据分页用了 OFFSET 方案改为游标分页按最后一条记录的 ID 查询日期显示差 8 小时后端存的是 UTC 时间前端按本地时区格式化时间戳分享出去的卡片没有图图片域名不在小程序白名单把图片 CDN 域名加入业务域名白名单安卓机键盘遮挡输入框adjust-position 属性没有设置监听键盘高度并动态调整底部安全区前端拿到 token 依然 401token 过期时间太短延长 JWT 有效期或加 refresh token 机制真机上页面白屏分包配置错误或组件路径不对检查 pages.json 的 subPackages 配置和组件路径6. 部署上线与安全加固经验6.1 服务器部署方案Nginx Gunicorn SupervisorFlask 开发服务器app.run()绝对不能用在生产环境这是所有 Flask 项目的第一步。我的部署方案是 Nginx 做反向代理和静态文件服务Gunicorn 跑 Flask 应用Supervisor 守护进程保证挂掉自动重启。部署流程整理一下方便第一次弄的人照着做# 1. 服务器上创建虚拟环境 python3 -m venv venv source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt # 3. 用 Gunicorn 启动应用 gunicorn -w 4 -b 127.0.0.1:5000 run:appGunicorn 的-w 4表示启动 4 个 worker 进程。在钓鱼这种偏低频的社区应用里4 个 worker 可以支撑的并发量已经很可观了。如果服务器 CPU 多核可以建一个gunicorn.conf.py配置文件把 worker 数和绑定端口都写进去。然后 Nginx 配置的关键是静态文件直接交给 Nginx 处理不要经过 Flask。图片上传目录 /uploads 直接映射到 Nginx 的alias这样图片加载完全不经 Python 进程性能差距极大。Supervisor 的配置也很简单核心就是让 Gunicorn 在后台跑着崩了自己拉起来[program:fishing] command/home/user/fishing/venv/bin/gunicorn -c gunicorn.conf.py run:app directory/home/user/fishing userwww-data autostarttrue autorestarttrue redirect_stderrtrue stdout_logfile/var/log/fishing.log6.2 HTTPS 与安全加固清单小程序发布后所有请求域名必须是 HTTPS这是微信的强制要求。我是用 Lets Encrypt 的免费证书配合 certbot 配置自动续期。这个步骤做完之后Nginx 配置文件里把 80 端口统一 301 重定向到 443。安全加固方面我做的工作包括上传文件的扩展名白名单校验防止上传可执行文件。图片文件重命名使用 UUID 替代用户原始文件名防路径穿越。JWT 的SECRET_KEY存在环境变量里不写进代码库。数据库连接用户单独创建权限只给应用需要的库不给超级管理员权限。请求体做大小限制Flask 里配置MAX_CONTENT_LENGTH防止大文件上传拖垮服务器。敏感接口都做频率限制防止短信接口被刷、发帖接口被灌垃圾内容。6.3 数据库备份与日志监控数据库和数据备份是很容易被忽略的环节。我做了两个层面的保障MySQL 每天凌晨自动备份到服务器本地然后同步到另一个存储空间日志上记录了 Nginx 访问日志和 Flask 应用日志排查问题时非常有用。备份命令一行搞定0 3 * * * /usr/bin/mysqldump -u backup_user -ppassword fishing_db /backup/fishing_$(date \%Y\%m\%d).sql配合 crontab 定时执行哪怕某天手滑删了数据也能恢复到前一天的快照。7. 项目复盘与个人心得做完这个项目有几个体会比较深。垂直社交的护城河不是技术而是场景理解。技术和架构讲完其实都不是什么新鲜东西。围绕地理位置做同城筛选、用户发布钓点内容、再到线下约钓成行这一连串场景的思考和串联才是真正有价值的部分。做 Flask 接口也好做 UniApp 适配也好底层都是套路但把技术贴合到钓鱼这个具体场景里的洞察需要自己积累。UniApp 跨端方案在这次项目里整体表现合格。开发和调试效率确实高但千万不要抱有“一套代码到处跑”的幻想。开发前期在模拟器里看到的效果到真机上都有概率出现渲染差异。我的建议是开发阶段就经常切真机调试尤其是布局和字体渲染这种问题越早暴露越省时间。后端接口的规范比想象中更重要。前后端并行开发时接口文档稍微不完整前端就要停下来等。后面我把接口文档升级成每个接口都标注请求参数、返回字段、错误码和示例联调效率一下子就上去了。这个小习惯值得在项目初期就养成。性能优化要跟着数据量走不要一开始就上重武器。Haversine 公式查重后的经纬度计算在现阶段完全够用真正到了数据量大再换空间索引也不迟。核心原则是先跑通业务再按实际瓶颈做优化。这个项目后续的扩展空间还不少。通讯录式的钓友关系链、基于鱼种和钓法的标签体系、群聊约钓、实时鱼情播报都是可以在现在这套架构上继续叠加的能力。如果有朋友也在做同类型的小程序或者同城社区欢迎来交流我还留着当初排坑时记的一堆笔记尤其是微信小程序审核和图片 CDN 加速那部分能帮你省不少时间。
返回列表