简介:基于 ThinkPHP5.1、Uniapp 与 MySQL5.7 开发的社区朋友圈源码,主要兼容 H5 端,后端采用 MVC 设计模式,前端基于 Uniapp 并使用 uview-UI 框架,图片压缩接入七牛云存储。这套源码面向想学习社区类程序开发的 PHP 与前端新手,也适合计划二次开发的读者;代码注释量大,可参考登录注册、动态发布、图片上传、接口路由等模块的实现思路,并借此扩展跑腿、支付等业务功能。资源包共 2000 个文件、体积约 9.47MB,JS 与 PHP 文件占据主体,分别对应前端交互与后端接口逻辑;另有 Vue 组件、JSON 配置、WXML/WXSS 小程序页面、图片与 GIF 素材等,覆盖页面样式、数据配置及多端适配资源。目前已有 543 人浏览学习。需要说明的是,作者实测该源码仅适合学习研究,无法直接商用部署,读者下载前请确认是否接受这一限制。
1. 拿到社区朋友圈源码,先拆的是 feed 流而不是界面
拿到这套 ThinkPHP 5.1 + Uniapp 的社区朋友圈源码时,我先拆的不是动态发布和点赞按钮,而是把数据库表结构全部导出来看了一遍。原因很简单:朋友圈这类时间线产品的代码量并不大,真正的门槛在 feed 流的查询模型设计,以及图片从上传、压缩到 CDN 回源的整条链路能不能扛住并发。这套项目恰好把这两块用最直白的方式摆在一个可运行的 demo 工程里,后端走 MVC 分层,前端用 Uniapp 搭配 uview-UI 组件库,数据库跑在 MySQL 5.7,主要兼容 H5 端。
作者在源码里保留了非常多的注释,适合用来理解一个社区类程序从请求到响应、再到外部对象存储的完整闭环;在这个基础上改出跑腿、支付这类带业务状态的模块,也比从零起框架快得多。需要先说明的是,这套代码是学习样本而不是商业成品,直接部署上线大概率会在鉴权、内容过滤、接口频率限制这些环节暴露出问题,后面我会把每一处坑的位置标出来。
2. 项目目录结构与 MVC 分层落点
2.1 后端 ThinkPHP 5.1 的目录映射关系
ThinkPHP 5.1 的 MVC 不是概念性的,它直接反映在application目录结构上。这套源码把index模块作为前端接口的唯一入口,控制器、模型、视图三个目录各司其职。我一般拿到 TP 项目会先看application/index/controller下面有哪些文件,因为控制器的命名直接对应路由前缀,比如Dynamic.php对应/index/dynamic这个 URL,Uniapp 端请求的接口路径就是从这儿来的。
application/ ├── index/ │ ├── controller/ │ │ ├── Dynamic.php # 动态发布/列表/删除 │ │ ├── Comment.php # 评论与回复 │ │ ├── User.php # 用户注册/登录/资料 │ │ └── Upload.php # 七牛 token 签发 │ ├── model/ │ │ ├── Dynamic.php │ │ ├── User.php │ │ └── Comment.php │ └── view/ # 管理端模板 ├── common.php # 公共函数 └── config/ # 应用配置前端走https://域名/index.php?s=/index/dynamic/lists这类入口文件格式,实际上 TP 5.1 默认支持 pathinfo 模式,只要在 Nginx 里配置好try_files就能去掉index.php。注意这里有个兼容点:Uniapp 编译到 H5 后请求的 baseURL 如果写成相对路径,在微信公众号里打开时会被当前页面路径覆盖,建议直接配置成完整域名加/index.php,哪怕不上伪静态也不会影响功能。
2.2 前端 Uniapp 工程与 uview-UI 的接入方式
前端项目在src或根目录下是一个标准 Uniapp 工程,页面集中在pages目录,公共组件和工具函数放在common与utils。uview-UI 的接入方式是典型的main.js全局挂载,这套源码用的还是 uview 1.x 版本,和 Vue 2 的 Uniapp 项目匹配,如果强行升级 Vue 3 需要换 uview-plus,这个话题我放到最后一章展开。
import uView from '@/uview-ui' Vue.use(uView) // 全局请求封装,统一处理 token 与错误码 uni.$u.http.setConfig({ baseURL: 'https://yourdomain.com/index.php', withCredentials: true }) uni.$u.http.interceptors.request.use(config => { const token = uni.getStorageSync('token') if (token) config.header.Authorization = token return config })这段代码的关键在两个地方:withCredentials: true是为了让 H5 端跨域请求带上 Cookie,因为 TP 5.1 的 session 认证依赖 Cookie,uniapp 打包成 App 时这个配置不生效,但 H5 必须开;拦截器里手动附加 token 是为了后续改成 JWT 认证预留位置。源码里大量使用uni.$u.http而不是直接uni.request,说明作者把 uview 的请求封装当作了统一出口,二开时新增接口只需要按同样的模式扩展。
2.3 核心数据表设计与字段含义
朋友圈业务的数据表不多,但每张表的字段都有讲究。我整理了一份核心表清单,顺序按照从主到从排列:
| 表名 | 主要字段 | 作用说明 |
|---|---|---|
| user | id, nickname, avatar, status | 用户基础信息,status 字段控制封禁状态 |
| dynamic | id, user_id, content, images, location, create_time | 动态主表,images 存 JSON 数组 |
| comment | id, dynamic_id, user_id, reply_id, content | 评论表,reply_id 支持楼中楼 |
| like_log | id, dynamic_id, user_id, create_time | 点赞记录表,唯一索引防止重复点赞 |
| user_follow | id, follower_id, following_id | 关注关系表,默认不启用 |
其中dynamic.images存 JSON 而不是单独建关联表,这是一个值得讨论的选型。单独建dynamic_image表看起来更范式化,但查询时多一次 join,而且图片数量固定为九宫格上限,JSON 字段配合json_decode完全够用,还能直接存进七牛返回的完整 URL 列表。缺点是统计图片数量需要count()函数在 PHP 层做,不能直接走 SQL,数据量上来后这是一个不可忽视的代价。
2.4 MySQL 5.7 环境与 TP 5.1 配置文件的对应关系
源码的数据库配置在application/database.php,TP 5.1 支持在.env文件中覆盖,部署时推荐用.env而不是直接改代码里的配置文件,避免把测试库连接串提交到 Git。
[database] hostname = 127.0.0.1 database = community username = root password = yourpass hostport = 3306 charset = utf8mb4 [七牛] access_key = your_ak secret_key = your_sk bucket = your_bucket domain = https://img.yourdomain.com这里注意charset = utf8mb4必须显式声明,如果数据库表本身是 utf8mb4 而连接字符集是 utf8,用户在朋友圈输入 emoji 时会把\xF0\x9F\x98\x80截断成乱码甚至直接报 SQL 错误——这是社区类项目里最常见的字符集事故。另外 MySQL 5.7 默认开启了ONLY_FULL_GROUP_BY,如果二开时写了GROUP BY user_id但 SELECT 里带了*,会直接抛 1055 错误,遇到这种情况先检查 sql_mode 而不是怀疑 SQL 语法。
3. 朋友圈时间线的查询模型与 TP 5.1 模型层实战
3.1 feed 流的核心难点是数据组装而不是查询
朋友圈时间线从产品形态上叫 feed 流,但在 MySQL 5.7 这个单库场景下根本碰不到复杂排序算法,最实际的做法就是dynamic表按create_time倒序分页,再批量补上用户信息和点赞状态。这里有一个新手容易踩的坑:在循环里写User::get($item['user_id'])会导致 N+1 查询,动态 20 条就是 21 条 SQL,后期接口响应时间直接线性恶化。
正确做法是先取出user_id数组,用一次whereIn查出所有用户信息,再在内存里组装成映射表。TP 5.1 的模型提供with预加载来屏蔽这个过程,但源码里用的是查询构造器配合数组映射,更直观也更容易调优。我给出的实现是集合式处理方案:
public function lists($page = 1, $limit = 10) { // 分页参数约束,防止负数或超大值打到数据库 $page = max(1, intval($page)); $limit = min(50, max(1, intval($limit))); // 动态主表数据,只需一次查询 $list = Db::name('dynamic') ->where('status', 1) ->order('create_time', 'desc') ->page($page, $limit) ->select() ->toArray(); if (empty($list)) { return []; } // 取出动态 id 和用户 id,准备批量查询 $dynamicIds = array_column($list, 'id'); $userIds = array_unique(array_column($list, 'user_id')); // 批量查用户信息,避免 N+1 $users = Db::name('user') ->whereIn('id', $userIds) ->column('nickname,avatar', 'id'); // 批量统计每条动态的评论与点赞数 $commentCount = Db::name('comment') ->whereIn('dynamic_id', $dynamicIds) ->group('dynamic_id') ->column('COUNT(*)', 'dynamic_id'); $likeCount = Db::name('like_log') ->whereIn('dynamic_id', $dynamicIds) ->group('dynamic_id') ->column('COUNT(*)', 'dynamic_id'); foreach ($list as &$item) { $item['user'] = $users[$item['user_id']] ?? []; $item['images'] = json_decode($item['images'], true) ?: []; $item['comment_count'] = $commentCount[$item['id']] ?? 0; $item['like_count'] = $likeCount[$item['id']] ?? 0; } return $list; }这段代码里whereIn加column是关键组合:column的第二个参数指定用id作为数组的 key,这样后面$users[$item['user_id']]的映射就是 O(1) 的数组索引。group('dynamic_id')配合column('COUNT(*)', 'dynamic_id')返回的是一个[dynamic_id => count]的关联数组,而不是二维表结构,这一步节省了 PHP 层的一次循环遍历。
3.2 分页参数设计与下拉刷新的配合方式
Uniapp 端朋友圈最常见的是 onPullDownRefresh 配合触底加载,对应到接口分页参数一般是page和limit,也可以叫start和count。源码采用的是前者,我认为这个选型对了,因为 TP 5.1 的page($page, $limit)方法直接就是页码与条数的语义,不需要在 SQL 里手动算 offset。
前端请求时的参数校验必须做在服务端,不能信任limit传来的 9999 这种值。上面代码里的min(50, max(1, intval($limit)))就是一道保险,把单页条数上限硬性压到 50。实战中还有一个细节:下拉刷新应该重置page=1并清空列表数组,触底加载则page+1并追加列表,如果后端没有在返回包里带上has_more字段,前端就得靠本次返回条数 < limit来判断是否到底,这套源码就是后者,二开时增加一个has_more字段会更稳。
3.3 动态删除与关联数据的清理
删除一条动态不只是删dynamic表那一行,还要带走它下面的评论、点赞记录,以及七牛上的图片原图。如果在删除时遗漏关联清理,用户会看到一个动态已消失但评论数和点赞数残留在统计接口里,这些脏数据会被后台统计报表计算进去,误导运营决策。
TP 5.1 的模型关联删除是一种常见做法,用hasMany定义好关联后写在删除逻辑里:
public function deleteDynamic($dynamicId) { // 事务保证多表删除的一致性 Db::startTrans(); try { // 查出 images 字段,删除七牛 OSS 文件 $dynamic = Db::name('dynamic')->where('id', $dynamicId)->find(); if (!empty($dynamic['images'])) { foreach (json_decode($dynamic['images'], true) as $fileUrl) { $this->deleteQiniuFile($fileUrl); } } // 删除动态主记录 Db::name('dynamic')->where('id', $dynamicId)->delete(); // 删除关联评论和点赞 Db::name('comment')->where('dynamic_id', $dynamicId)->delete(); Db::name('like_log')->where('dynamic_id', $dynamicId)->delete(); Db::commit(); return true; } catch (\Exception $e) { Db::rollback(); throw $e; } }注意事务只能保证数据库的一致性,删除七牛文件失败不会回滚数据库,因为外部存储不在事务边界内。常见的妥协策略是:先删数据库,再删 OSS 文件,OSS 删除失败打日志由定时任务重试。如果使用模型关联定义comment和likeLog两个hasMany,可以用with('comment,likeLog')->delete()自动执行关联删除,但 TP 5.1 的关联删除每处理一个关联都会发一次 SQL,数据量大时需要自己权衡。
3.4 SQL 注入防护与 TP 5.1 的查询构造器边界
TP 5.1 的查询构造器默认做了参数绑定,where里的值传入不会直接拼接 SQL,所以常规注入已经被框架拦截。但有一个例外:order和field方法不支持参数绑定,如果你二开时把前端传来的排序字段直接拼进去,就会打开注入窗口。
// 危险写法:$sort 直接拼进 order Db::name('dynamic')->order($sort)->select(); // 安全写法:白名单校验 $allowedSort = ['create_time' => 'desc', 'like_count' => 'desc']; $order = $allowedSort[$sort] ?? ['create_time' => 'desc']; Db::name('dynamic')->order($order)->select();如果要在 PHP 8 环境下运行 TP 5.1 老项目,还需要注意一个兼容性细节:PHP 8.0 移除了each()函数,TP 5.1 的部分底层代码在特定场景会触发崩溃,建议直接通过 Composer 升级topthink/framework到 5.1 的最新维护版本,而不是停在最初发布的版本。另外 MySQL 5.7 对 group by 的 strict 模式前文提过,二开聚合查询时尽量只查询分组字段和聚合函数字段。
4. 七牛云存储接入与图片压缩链路
4.1 为什么社区类项目把图片上云放在第一优先级
朋友圈动态的核心内容是图片,本地服务器存储图片最大的问题不是磁盘容量,而是带宽。单张 2MB 的图片在一台 5Mbps 的服务器上要 3 秒才能加载完,用户滑动时间线时并发拉取几十张图片直接打满出口带宽。源码选择七牛云存储解决了三个问题:上行带宽释放、CDN 加速回源、图片实时压缩。
本地只保留原始上传请求,Uniapp 端把图片交给七牛后,后端返回的只是七牛文件 key,后续页面加载的 URL 全走七牛 CDN 域名。动态发布接口在写入数据库时,图片字段存的就是https://img.yourdomain.com/xxxxx.jpg这种完整 URL,这样查询端不用再做一次拼接。七牛在就近 CDN 节点缓存图片,用户在多地域访问的延迟差异能控制在一倍以内。
4.2 上传凭证签发与回调校验的完整流程
七牛的上传凭证(uploadToken)不能由客户端直接生成,因为签发凭证需要 SecretKey,它只能放在 PHP 后端。源码在Upload控制器里实现了这个逻辑,Uniapp 端先请求/index/upload/token拿到凭证,再凭这个 token 直接上传文件到七牛。
public function token() { $accessKey = config('qiniu.access_key'); $secretKey = config('qiniu.secret_key'); $bucket = config('qiniu.bucket'); // 自定义上传策略,控制文件大小与 MIME 类型 $policy = [ 'scope' => $bucket, 'deadline' => time() + 3600, // token 有效期 1 小时 'mimeLimit' => 'image/jpeg;image/png;image/webp;image/gif', 'fsizeLimit' => 5242880, // 单文件最大 5MB 'returnBody' => '{"key":"$(key)","hash":"$(etag)","w":"$(imageInfo.width)","h":"$(imageInfo.height)"}', 'callbackUrl' => 'https://yourdomain.com/index/upload/callback', 'callbackBody' => 'key=$(key)&hash=$(etag)&uid=' . $this->uid ]; // 签名算法:Base64 URL 安全的编码 + HMAC-SHA1 $encodedPolicy = $this->base64UrlEncode(json_encode($policy)); $sign = hash_hmac('sha1', $encodedPolicy, $secretKey, true); $encodedSign = $this->base64UrlEncode($sign); $token = $accessKey . ':' . $encodedSign . ':' . $encodedPolicy; return json(['token' => $token, 'domain' => config('qiniu.domain'), 'expires' => 3600]); }这里最关键的是mimeLimit与fsizeLimit,如果漏掉这两个参数,七牛会接受任意文件类型与任意大小,攻击者可以把服务器存储当作免费网盘,往 bucket 里拖几个 GB 的文件然后跑路。deadline控制 token 有效期,前端在 onShow 时重新获取比保存一个 token 复用一天更安全,因为过期后上传会直接 401。
callbackUrl与callbackBody的作用是:七牛收到文件后回调你的业务服务器,把$(key)、$(etag)连同 URL 编码后的自定义字段传过来,后端在这个接口里可以执行动态写入数据库的操作。注意如果你只配置returnBody而不配置callbackUrl,用户上传成功后七牛直接把returnBody返回给客户端,后端拿不到任何回调,这适合客户端再单独请求一次后端接口完成业务入库的模式。
4.3 图片压缩参数与样式分隔符的选型
七牛云存储的图片压缩不用改原图,它通过 URL 上的处理参数动态生成缩略图。这套源码用的处理参数基于 imageMogr2,核心是 thumbnail 和 quality 两个参数:
| 参数 | 示例 | 效果 | 适合场景 |
|---|---|---|---|
| thumbnail | ?imageMogr2/thumbnail/600x | 按宽度等比缩放,高度自动 | 朋友圈列表图 |
| quality | ?imageMogr2/quality/80 | 压缩质量到 80% | 减少体积,观感损失极小 |
| format | ?imageMogr2/format/webp | 转 webp 格式 | H5 端 Chrome 与微信内置浏览器 |
| auto-orient | ?imageMogr2/auto-orient | 修正 EXIF 旋转 | 手机上传的图片必须加 |
实际存储在数据库里的 URL 不要带这些参数,查询时在 PHP 层统一拼接,二开时如果要修改压缩策略只需要改一个公共函数,不用去数据库里把每一条 URL 都刷一遍。H5 端使用<image>组件加载拼接后的 URL,就能展示压缩后的图片。多尺寸需求(列表小图、详情大图、头像裁切)可以在公共函数里做一个尺寸映射表,前端传场景名进来返回对应 URL,比前端自行拼参数更可控。
// 七牛图片样式拼接函数 function qiniu_thumb($url, $scene = 'list') { $rules = [ 'list' => '?imageMogr2/thumbnail/600x/quality/80', 'detail' => '?imageMogr2/thumbnail/1080x/quality/85', 'avatar' => '?imageMogr2/thumbnail/200x200/quality/80' ]; $rule = $rules[$scene] ?? $rules['list']; return $url . $rule; }这种方式比七牛控制台里配的「样式分隔符」(例如-list后缀)更灵活,因为分隔符方案需要提前在控制台定义样式,二开时临时加一种尺寸得去控制台操作,代码本身不可追溯。用 PHP 侧函数拼接的话,所有规则沉淀在代码里,Git 记录就是文档。
4.4 H5 直传的 CORS 与 token 刷新策略
Uniapp 编译成 H5 后,uni.uploadFile请求发到七牛域名属于典型跨域请求,七牛 bucket 需要开启 CORS 规则,允许来源填写你的前端域名,允许方法写GET, POST, PUT, OPTIONS,允许请求头写Content-Type, Authorization, X-Requested-With。如果漏配 CORS,浏览器会拦截 preflight 请求或最终响应,表现为上传一直转圈然后 fail。
还需要注意一个 token 过期边界:七牛 token 有效期 60 分钟,用户在文章编辑页停留超过这个时间再提交图片就会收到 401。常见做法是组件在每次上传前先校验本地 token 的过期时间戳,小于 5 分钟就重新请求,而不是依赖后端全局态。
5. H5 兼容、小程序打包与二开扩展实践
5.1 Uniapp 编译 H5 后嵌入公众号遇到的授权与定位问题
这套源码主要兼容 H5,实际跑起来最常见的场景是嵌入微信公众号菜单里。一旦作为网页授权应用,微信内置浏览器的window.location.href跳转会丢失 session 状态,如果源码中登录用 Cookie 做会话保持,用户每次进入都可能被判定为未登录。所以二开的第一步是把 TP 侧登录态改成 token 机制,Uniapp 端把 token 放入每次请求的Authorization头,由TP的中间件统一校验。
微信公众号里获取地理位置需要 JS-SDK 权限,Uniapp 端通过<script>标签加载微信的jweixin-1.6.0.js,然后调用wx.config注入签名信息。这里签名必须由后端用noncestr、timestamp、url三个参数计算,url必须是当前页面完整路径含 hash,小程序里由uni.getLocation直接调用即可,H5 则只能走 JS-SDK。
// uniapp H5 页面中初始化微信 JS-SDK // #ifdef H5 const wx = require('@/utils/jweixin.js') uni.request({ url: 'https://yourdomain.com/index/wechat/signature', data: { url: location.href.split('#')[0] }, success: (res) => { wx.config({ debug: false, appId: res.data.appId, timestamp: res.data.timestamp, nonceStr: res.data.nonceStr, signature: res.data.signature, jsApiList: ['getLocation'] }) } }) // #endif注意location.href.split('#')[0]这个细节:必须要 strip 掉 hash。微信签名校验的 URL 是不带 hash 的,如果原样传入,签名验证会失败并报 invalid signature,这是社区里提问率很高的一个坑。另外签名接口不能放缓存,后端每次都要用当前的timestamp重新计算,计算完传给前端后签名只在 5 分钟内有效。
5.2 Uniapp 打包微信小程序时的几个可复用经验
Uniapp 打包成微信小程序时,这套基于 H5 优先的源码有几个必须处理的点。首先是 uview-UI 1.x 在微信小程序端的 CSS 兼容问题——rpx单位在组件内部已经处理好,但如果你二开新增的页面直接写了px,在小程序端会偏小,建议所有新页面统一使用 rpx。其次是小程序端的uni.uploadFile不能直接带Authorization头,微信会在一些场景下过滤自定义 header,需要改用uni.request携带临时文件路径,或者在小程序的后台配置里为七牛域名开启「合法域名」白名单。
小程序的图片上传前还有本地临时文件的问题:chooseImage拿到的路径是wxfile://临时协议,不能直接拿来展示在<image>中,需要先用uni.saveFile转存或者接受临时路径的有效期限制。如果要实现 H5 与小程序双端同时支持朋友圈发布,上传逻辑应该独立封装成一个混合上传方法,根据// #ifdef H5与// #ifdef MP-WEIXIN分别执行不同分支。
5.3 从朋友圈动态延伸出跑腿、支付模块的改造方向
源码本身是社区朋友圈,但作者在摘要里点明了它可以扩展成跑腿、支付这类带交易状态的程序。朋友圈的核心数据模型是一个无状态内容流,跑腿订单则是典型的状态机流转:待接单、已接单、配送中、已完成、已取消。改造的关键不是把dynamic表改造成订单表,而是新建order表并建立与dynamic的弱关联(订单备注可以发成一条动态)。
支付接入微信支付时,后端notify回调接口必须放在独立控制器中且关闭鉴权,因为微信服务器不会携带你的业务 token。在回调里验签成功后要更新订单状态与用户余额,这两步操作必须放在事务里,而且要保证幂等——微信会多次推送同一笔订单的回调,不做幂等控制就会出现用户余额被加两次的事故。支付回调并不会阻塞核心业务,建议用队列异步处理订单后续动作。
5.4 ThinkPHP 5.1 老项目的安全加固清单
TP 5.1 在互联网上已公开多种已知漏洞利用方式,部署到外网前必须做三件事:第一,访问runtime目录禁止 Web 访问,Nginx 配置里直接 location 拒掉runtime/与thinkphp/;第二,删除不必要的index.php入口文件暴露信息,应用部署目录与 Web 根目录分离是更标准的做法;第三,升级到框架最新维护版,因为旧版本对Request对象的filter[]参数处理存在 SQL 注入隐患。
如果要从 PHP 7.1 迁移到 PHP 8.0,TP 5.1 源码中可能触发弃用告警的写法集中在each()、create_function()与money_format()。建议在本地逐一跑php -l语法检测,再对关键接口做一轮回归测试。若运维环境有更高的 PHP 要求,优先考虑 TP 5.1 最后一个兼容版本或整体升级到 TP 6。
6. 部署验证清单与 uniapp vue2 转 vue3 的注意点
6.1 从 Vue 2 迁移到 Vue 3 时需要处理的兼容面
这套源码是基于 uview 1.x 与 Vue 2 的 uniapp 项目,如果哪天想把它迁到 Vue 3 版本,先别急着改 UI 组件。uview 1.x 在 Vue 3 下直接不可用,常规选择是替换为uview-plus,它的 API 与 uview 1.x 大致对齐,但少量属性名称有调整,比如u-parse组件改成了内置组件rich-text,按钮plain变更为variant。迁移前建议全局搜索u-开头的组件使用点,逐一比对新老文档。
Vue 3 里this的访问方式发生重大变化,原本写在methods中的this.$u.http在组合式 API 中要从setup返回,或者通过getCurrentInstance获取全局属性。操作这类老项目时的常用做法是在main.js里保留一个中间层兼容对象,确保业务代码迁移时改动面可控。
6.2 部署后应执行的四项验证
拿到源码部署上线后,我建议按如下顺序完成四步验证,每一步都能定位一类问题:
| 验证项 | 具体操作 | 预期结果 |
|---|---|---|
| 数据库初始化 | 导入sql文件,检查user、dynamic表是否有测试数据 | 能正常查询,表中无乱码 |
| 七牛上传链路 | 在 H5 端选择图片发布动态 | 图片 URL 成功写入dynamic.images字段 |
| H5 定位 | 在微信开发者工具中打开页面,模拟授权 | 能正常弹出授权并拿到经纬度 |
| 小程序打包 | HBuilderX 云端打包,上传到微信开发者工具预览 | 页面正常加载,图片走 CDN 域名可访问 |
第二项最容易踩坑:如果七牛的bucket没有设置returnBody而你在 PHP 侧等待$(key),上传回调会返回空字符串,Uniapp 端表现为上传成功但列表页不显示。第三项适合用开发者工具的真机调试功能验证,因为 PC 模拟器中的定位数据是由微信调试工具注入的,和真机行为有差异。
6.3 常见报错的快速定位步骤
七牛上传报 401 时最优先排查的不是access_key,而是服务器的系统时间是否偏移超过 5 分钟。七牛签名校验依赖 Unix 时间戳,如果 NTP 没同步,deadline已经是过去时间,签名必然被拒。用date +%s对比标准时间,偏差过大即处理。
动态列表接口返回 500 且页面无任何输出,八成是application/database.php里debug关闭导致的错误信息被吞。临时打开'show_error_msg' => true后重新请求,把报错内容截图处理完再关掉。
Uniapp 端出现「页面路径找不到」时检查pages.json的注册顺序与manifest.json中 H5 路由模式配置。history路由在 Nginx 直接访问二级页面会 404,需要在 location 中配置try_files $uri $uri/ /index.html;,或者把路由模式改回hash一劳永逸——这也是 H5 版朋友圈最常见的部署问题。
本文还有配套的精品资源,点击获取