主题
在线报修与无感刷新(三)
本页对应 在线报修与无感刷新,共 5 段视频,约 1 小时 7 分。
学习目标
- 能够说出小程序保存网络图片时必须先调用
wx.getImageInfo获取本地临时路径、再调用保存相册 API 的原因,并独立完成从配置 downloadFile 合法域名到点击按钮成功保存图片的完整实现。 - 能够画出小程序授权处理流程图(检测授权状态 → 未授权时弹窗引导 → 打开设置页 → 返回后重新检测 → 已授权则调用业务 API),并据此封装出带授权检测的保存图片函数,区分已授权、拒绝、二次拒绝三种分支行为。
- 能够说明双 token 机制中普通 token 与 refresh token 的有效期差异及各自职责,并在响应拦截器中依据 401 状态码识别 token 过期,发起刷新请求后用返回的新值同步更新应用状态与本地存储。
- 能够通过修改
config.headers覆盖原请求头中的过期 token,并重新发起原请求,使用户在页面无感知的情况下看到数据正常渲染。 - 能够识别 refresh token 同样过期时产生的请求死循环,并通过判断请求 URL 是否为刷新接口来终止递归、清除本地 token 并携带回跳地址重定向到登录页。
- 能够在调试中通过断网或修改代码快速定位并中止请求死循环,说明此类循环为何属于会导致应用崩溃的严重缺陷。
知识点
访客详情的两条入口
为什么需要:访客详情页既可能由已登录用户从列表进入,也可能由朋友通过分享卡片打开。前者能拿到访客 id,后者拿不到登录态,只能依赖分享链接里的 encryptedData。因此 onLoad 必须同时识别两种参数。
怎么想:先看 URL 里有什么。有 id 走登录态详情接口;有 encryptedData 走分享详情接口。两个请求都遵循“先判参数、再请求、再判 code、最后 setData”的固定节奏。
怎么做:在 onLoad 中解构参数,分别调用两个方法。核心代码如下:
javascript
// visitor_pkg/pages/passport/index.js
Page({
data: {
passport: {}
},
onLoad({ id, encryptedData }) {
// 获取访客详情
this.getPassport(id)
this.getPassportShare(encryptedData)
},
// 获取访客详情(通行证)
async getPassport(id) {
// 检测是否存在 id
if (!id) return
// 调用接口
const { code, data: passport } = await wx.http.get('/visitor/' + id)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ passport })
},
async getPassportShare(encryptedData) {
// 检测是否存在 encryptedData
if (!encryptedData) return
// 调用接口
const { code, data: passport } = await wx.http.get('/visitor/share/' + encryptedData)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ passport })
},
})易错点:两个方法都要先判空再发请求,不能把 id 和 encryptedData 混用;接口路径不同,/visitor/ 与 /visitor/share/ 不能写串;code !== 10000 时直接 toast 并返回,避免把错误数据渲染到页面。
登录态按 ID 获取并渲染二维码
为什么需要:详情页要展示通行证二维码、有效时间和房屋信息。二维码有时效性,过期后 validTime 为 -1,页面必须切换到“二维码失效”状态,而不是继续倒计时。
怎么想:接口返回的 passport 直接进入 data;模板用 wx:if 判断 validTime > 0 才展示倒计时组件,否则展示 00:00:00。二维码区域在 validTime === -1 时加半透明蒙层。
怎么做:保留笔记中的渲染代码,注意倒计时 time 要乘 1000,因为接口字段是秒:
xml
<view class="passport">
<view class="countdown">
<van-count-down wx:if="{{passport.validTime > 0}}" time="{{passport.validTime * 1000}}" />
<view wx:else class="van-count-down">00:00:00</view>
<view class="label">通行证有效时间</view>
</view>
<view class="qrcode">
<image src="{{passport.url}}"></image>
<view wx:if="{{passport.validTime === -1}}" class="mask">二维码失效</view>
</view>
<view class="description">
<view class="house">{{passport.houseInfo}}</view>
<view class="tips">将此二维码分享给访客,访客扫码即可开门</view>
</view>
</view>易错点:validTime 是秒,倒计时组件按毫秒,漏乘 1000 会显示异常;过期判断用 === -1,不要写成 > 0 的反向逻辑导致蒙层条件错;图片地址是 passport.url,不要误写成固定路径。
分享页通过 encryptedData 获取详情
为什么需要:分享给朋友后,新用户可能未登录,不能按 id 查详情。分享时把 encryptedData 拼到路径后面,接收页再用它换取详情。
怎么想:正常详情数据里带有 encryptedData,分享路径就拼它;分享页请求 /visitor/share/:encryptedData。工具栏的显示条件利用“正常详情有该字段、分享接口不返回该字段”来控制。
怎么做:自定义分享与工具栏条件如下:
javascript
// visitor_pkg/pages/passport/index.js
Page({
data: {},
onLoad() {},
// 自定义分享
onShareAppMessage() {
// 获取加密数据
const { encryptedData } = this.data.passport
return {
title: '查看通行证',
path: '/visitor_pkg/pages/passport/index?encryptedData=' + encryptedData,
imageUrl: 'https://enjoy-plus.oss-cn-beijing.aliyuncs.com/images/share_poster.png',
}
},
})xml
<view class="toolbar" wx:if="{{passport.encryptedData}}">
<button class="button-share" open-type="share">
<text class="enjoy-icon icon-share"></text>
<text class="text">分享给朋友</text>
</button>
<button class="button-save">
<text class="enjoy-icon icon-save"></text>
<text class="text">保存到本地</text>
</button>
</view>易错点:分享路径参数名必须是 encryptedData,不要写成 encrypteddata;分享页不要再让用户二次分享,工具栏条件要结合接口返回字段核对;onShareAppMessage 里从 this.data.passport 取加密数据,取不到时要先确认详情是否已渲染。
保存到本地与授权处理(转写稿补充)
为什么需要:二维码是在线图片,saveImageToPhotosAlbum 不支持网络路径,直接保存会失败。首次保存还会触发相册授权,用户点“拒绝”后必须引导去设置页开启。
怎么想:先 wx.getImageInfo 把网络图下载到本地临时路径,再 wx.saveImageToPhotosAlbum 保存;保存前用 wx.getSetting 检测相册权限,拒绝时先弹提示,再 wx.openSetting 引导。
怎么做:转写稿中的调试顺序是:配置 downloadFile 合法域名 → 点击保存 → 打印 getImageInfo 返回的临时路径 → 把 filePath 传给保存相册 API → 补授权检测。授权检测建议用回调写法,避免 getSetting 的 Promise 写法在打开设置页时出问题。
易错点:downloadFile 域名没配会导致下载失败;保存参数是 filePath,不是网络地址;用户拒绝后不要反复弹窗打扰;无感刷新场景中,刷新 token 的请求自身返回 401 时要先判断并终止,避免死循环。
本节目标与问题
本节要完成的是:在访客详情页点击“保存到本地”,把页面上的二维码图片保存到系统相册。这里不能像网页端那样用一个 a 标签加 href 直接下载,因为小程序对文件与相册权限有专门限制。转写稿里先直接查看保存 API 的文档,发现它要求的是本地文件路径,不支持网络路径,所以必须先把在线图片“取到本地”,再保存。
为什么需要:二维码是在线图片,保存 API 不能直接吃网络地址。
怎么想:先下载/获取图片信息,拿到临时本地路径,再调用保存相册。
怎么做:用 wx.getImageInfo 获取图片信息,用 wx.saveImageToPhotosAlbum 保存。
易错点:把 src 写成网络图片地址后,误以为返回结果就是图片本身;实际要取返回的 path 再保存。
获取图片信息:wx.getImageInfo
wx.getImageInfo 通过图片路径读取图片信息。转写稿中调用后,控制台能看到图片宽度、高度、类型,以及一个 tmp 开头的临时本地路径。这个临时路径就是后续保存要用的 filePath。
为什么需要:保存相册 API 需要本地路径,网络图片不能直接保存。
怎么想:把“保存”拆成“先获取本地路径,再写入相册”两步。
怎么做:
javascript
wx.getImageInfo({
src: 'https://domain.com/path/xxx.jpg',
success(result) {
console.log(result)
}
})易错点:转写稿里提到的“单录域名”应理解为 download 域名;不配置会导致下载/获取图片信息失败。配置后还要在开发者工具里刷新项目配置再测试。
配置 download 域名
为什么需要:小程序出于安全限制,下载网络资源需要配置合法域名。
怎么想:二维码从哪个域名下载,就把哪个域名加入 download 合法域名。
怎么做:在小程序管理后台“开发管理 → 开发设置”里添加 download 域名,回到开发者工具刷新项目配置。
易错点:只配置了 request 域名不够;转写稿中特意强调“从哪里下就配哪一个路径”,不要配错域名。
保存到相册:wx.saveImageToPhotosAlbum
wx.saveImageToPhotosAlbum 将图片保存到相册。第一次调用会弹出授权窗口,用户允许后即可保存;转写稿测试时,模拟器会让人选择文件夹,手机上则直接保存到系统相册,二者有一点差异。
为什么需要:最终目标是把二维码写入用户相册。
怎么想:把上一步拿到的本地 path 传给 filePath。
怎么做:
javascript
wx.saveImageToPhotosAlbum({
filePath: '待保存的图片路径(通过 wx.getImageInfo 读取到的)',
success(result) {
console.log(result)
}
})易错点:filePath 不能填网络地址;第一次调用要处理授权弹窗,用户点拒绝后不能继续保存。
组合实现保存二维码
页面结构中先绑定点击事件:
xml
<view class="toolbar" wx:if="{{passport.encryptedData}}">
<button class="button-share" open-type="share">
<text class="enjoy-icon icon-share"></text>
<text class="text">分享给朋友</text>
</button>
<button bind:tap="saveQRCode" class="button-save">
<text class="enjoy-icon icon-save"></text>
<text class="text">保存到本地</text>
</button>
</view>业务代码里组合两个 API:
javascript
// visitor_pkg/pages/passport/index.js
Page({
data: {},
onLoad() {},
// 保存二维码
async saveQRCode() {
try {
// 读取图片信息
const { path } = await wx.getImageInfo({
// 二维码的图片路径
src: this.data.passport.url,
})
// 保存图片到相册
wx.saveImageToPhotosAlbum({ filePath: path })
} catch (err) {
wx.utils.toast('保存图片失败,稍后重试!')
}
}
})为什么需要:单独调用任一 API 都不够,必须串起来。
怎么想:getImageInfo 负责“拿到本地路径”,saveImageToPhotosAlbum 负责“写入相册”。
怎么做:用 async/await 让两步顺序执行,并加 try/catch 兜底。
易错点:转写稿里调试时先打印 result,确认 path 存在再保存;不要直接把 src 传给 filePath。
授权流程:拒绝后如何引导
转写稿继续完善了授权处理:首次保存会弹授权窗口,点“允许”后以后可直接保存;如果点“拒绝”,再次保存会失败。此时不能反复弹窗打扰用户,而要先检测授权状态,再引导用户打开设置。
为什么需要:用户可能误点拒绝,需要可恢复路径。
怎么想:先 wx.getSetting 检测相册写入权限;未授权时用 wx.showModal 说明原因,再用 wx.openSetting 打开设置页;用户返回后再次检测,仍拒绝就不继续打扰。
怎么做:转写稿中权限字段是 scope.writePhotosAlbum,因为字段带点,访问时用中括号:authSetting['scope.writePhotosAlbum']。检测到已授权就直接保存;未授权先提示“请允许添加到相册”,再打开设置。
易错点:转写稿提到 getSetting 用 Promise 方式时打开设置界面可能出问题,因此演示中改用回调写法;清除授权数据可在开发者工具“清除缓存 → 清除授权数据”里测试首次弹窗和拒绝分支。不要写成死循环式反复弹窗。
先明确本节目标:登录态下取访客列表
为什么需要:访客列表是访客管理模块的入口数据,用户保持登录状态时,页面要能拿到自己的访客记录;接口支持分页,后续还要接滚动加载。接口文档地址见笔记:接口文档地址。
怎么想:先不要一上来做分页,先把“不分页也能取到第一批数据”跑通。页面 onLoad 时调用列表接口,默认 current=1、pageSize=5;接口返回后先判断 code,成功再 setData,同时用 isEmpty 标记空列表。
怎么做:
javascript
// visitor_pkg/pages/list/index.js
Page({
data: {
visitorList: [],
isEmpty: false,
},
onLoad() {
// 获取访客列表
this.getVisitorList()
},
// 访客列表接口
async getVisitorList(current = 1, pageSize = 5) {
// 调用接口
const {
code,
data: {rows: visitorList},
} = await wx.http.get('/visitor', {current, pageSize})
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({
isEmpty: visitorList.length === 0,
visitorList,
})
},
})易错点:code !== 10000 时不要继续渲染;isEmpty 要按本次返回的 visitorList.length 判断;分页参数名是 current、pageSize,不要拼错。转写稿里反复出现 token 过期、刷新 token 的调试,这说明列表接口依赖有效登录态;但无感刷新属于统一拦截器内容,本节不展开,先把列表主流程跑通。
列表渲染与空状态
为什么需要:有数据时要展示访客房屋信息、姓名、手机号、访问日期和状态;没有数据时要给用户明确提示和“添加”入口,不能只留白屏。
怎么想:外层用 wx:if="{{!isEmpty}}" 控制列表,wx:else 展示空状态;列表项用 wx:for 和 wx:key="id"。状态字段用 item.status 判断,配合 success、cancel 两个类名。
怎么做:
xml
<block wx:if="{{!isEmpty}}">
<scroll-view show-scrollbar="{{false}}" enhanced scroll-y>
<view class="visitor">
<view class="visitor-title">我的访客</view>
<view class="visitor-list">
<van-cell-group
wx:for="{{visitorList}}"
wx:key="id"
border="{{false}}"
bind:tap="goPassport"
>
<van-cell size="large" title="{{item.houseInfo}}">
<text wx:if="{{item.status === 1}}" class="tag success">生效中</text>
<text wx:if="{{item.status === 0}}" class="tag cancel">已失效</text>
</van-cell>
<van-cell title="访客姓名" border="{{false}}" value="{{item.name}}" />
<van-cell title="手机号码" border="{{false}}" value="{{item.mobile}}" />
<van-cell title="访问日期" border="{{false}}" value="{{item.visitDate}}" />
</van-cell-group>
</view>
</view>
</scroll-view>
</block>
<view wx:else class="blank">
您还没有访客记录,请点击
<navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>易错点:笔记文字写“0 表示生效中、1 表示已失效”,但示例代码里 status === 1 显示“生效中”、status === 0 显示“已失效”,两者相反。落地时必须以接口文档或真实返回为准核对,不能照抄矛盾处。另外 wx:key 要用唯一字段,空状态里的 navigator 路径不要写错。
分页加载与滚动触底
为什么需要:访客可能很多,一次全量取不现实;接口支持分页,就要在用户滚动到底部时继续取下一页。
怎么想:给 scroll-view 加 bind:scrolltolower="getMoreVisitor",触底时请求下一页;但触底事件可能短时间重复触发,所以笔记要求用节流函数避免无意义请求,节流可用第三方库 miniprogram-licia。
怎么做:在原来的 scroll-view 上补事件:
xml
<block wx:if="{{!isEmpty}}">
<scroll-view
bind:scrolltolower="getMoreVisitor"
show-scrollbar="{{false}}"
enhanced
scroll-y
>
<view class="visitor">
....
</view>
</scroll-view>
</block>
<view wx:else class="blank">
您还没有访客记录,请点击
<navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>笔记只给出节流思路和库名,没有给出完整 getMoreVisitor 实现,这里不补造代码。实现时应在 getMoreVisitor 中维护当前页码,请求成功后把新数据追加到 visitorList,并继续更新 isEmpty。
易错点:分页请求不要把旧列表直接覆盖,除非是第一页;触底重复触发要用节流;请求失败要 toast,不要静默;当返回空数组时不要无限请求下一页。转写稿中保存图片、授权、无感刷新等属于相邻业务,本节只聚焦“列表接口—渲染—分页触底”这条主线。
课件图示







代码示例
示例 1:访客详情页(登录态 + 分享态)
javascript
// visitor_pkg/pages/passport/index.js
Page({
data: {
passport: {}
},
onLoad({ id, encryptedData }) {
// 情况 1:登录状态,根据 id 获取访客详情
this.getPassport(id)
// 情况 2:通过分享打开,根据 encryptedData 获取访客详情
this.getPassportShare(encryptedData)
},
// 获取访客详情(通行证)
async getPassport(id) {
// 检测是否存在 id
if (!id) return
// 调用接口
const { code, data: passport } = await wx.http.get('/visitor/' + id)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ passport })
},
// 分享打开时获取访客详情
async getPassportShare(encryptedData) {
// 检测是否存在 encryptedData
if (!encryptedData) return
// 调用接口
const { code, data: passport } = await wx.http.get('/visitor/share/' + encryptedData)
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ passport })
},
// 自定义分享
onShareAppMessage() {
// 获取加密数据
const { encryptedData } = this.data.passport
return {
title: '查看通行证',
path: '/visitor_pkg/pages/passport/index?encryptedData=' + encryptedData,
imageUrl: 'https://enjoy-plus.oss-cn-beijing.aliyuncs.com/images/share_poster.png',
}
},
})xml
<!-- visitor_pkg/pages/passport/index.wxml -->
<view class="passport">
<view class="countdown">
<van-count-down wx:if="{{passport.validTime > 0}}" time="{{passport.validTime * 1000}}" />
<view wx:else class="van-count-down">00:00:00</view>
<view class="label">通行证有效时间</view>
</view>
<view class="qrcode">
<image src="{{passport.url}}"></image>
<view wx:if="{{passport.validTime === -1}}" class="mask">二维码失效</view>
</view>
<view class="description">
<view class="house">{{passport.houseInfo}}</view>
<view class="tips">将此二维码分享给访客,访客扫码即可开门</view>
</view>
</view>
<view class="toolbar" wx:if="{{passport.encryptedData}}">
<button class="button-share" open-type="share">
<text class="enjoy-icon icon-share"></text>
<text class="text">分享给朋友</text>
</button>
<button bind:tap="saveQRCode" class="button-save">
<text class="enjoy-icon icon-save"></text>
<text class="text">保存到本地</text>
</button>
</view>要点解释与易错点:
onLoad({ id, encryptedData })直接从页面参数中解构:带id说明是登录态进入,带encryptedData说明是分享进入,两个方法都写了,靠方法内部的if (!id) return/if (!encryptedData) return各自短路,互不干扰。const { code, data: passport } = await wx.http.get(...)用了对象解构重命名,data直接改名为passport,省一次中间变量;接口约定code === 10000才算成功,失败要return,否则后面的setData会把undefined写进data。- 倒计时组件
van-count-down的time单位是毫秒,所以validTime * 1000;当validTime === -1表示二维码已失效,此时不展示倒计时(走wx:else显示00:00:00),同时在二维码上叠一层.mask蒙层。 - 分享按钮靠
open-type="share"触发onShareAppMessage,不需要自己写点击事件;path里必须把encryptedData拼在 URL 后面,否则被分享的人拿不到数据。 - 笔记中
wx:if="{{passport.encryptedData}}"用于控制工具栏显隐(分享打开的场景下不应再允许二次分享),实际业务里请对照接口返回的字段确认这个判断方向是否正确,字段名/取反写错会导致按钮该隐藏时没隐藏。
示例 2:保存二维码到本地相册
javascript
// visitor_pkg/pages/passport/index.js
Page({
data: {
passport: {}
},
onLoad({ id, encryptedData }) {
this.getPassport(id)
this.getPassportShare(encryptedData)
},
async getPassport(id) { /* 见示例 1 */ },
async getPassportShare(encryptedData) { /* 见示例 1 */ },
// 保存二维码
async saveQRCode() {
try {
// 第一步:读取图片信息(本质是把网络图片下载到本地)
const { path } = await wx.getImageInfo({
// 二维码的图片路径
src: this.data.passport.url,
})
// 第二步:保存图片到相册
await wx.saveImageToPhotosAlbum({ filePath: path })
wx.utils.toast('保存成功')
} catch (err) {
wx.utils.toast('保存图片失败,稍后重试!')
}
},
})要点解释与易错点:
wx.saveImageToPhotosAlbum的filePath不支持网络路径,直接把https://...丢进去会失败,所以必须先用wx.getImageInfo把网络图"下载"成本地临时路径(返回值里的path,即 tmp 临时路径),再拿这个本地路径去保存。wx.getImageInfo需要在小程序管理后台 开发管理 → 开发设置 → 服务器域名 里把二维码图片所在的域名加到downloadFile合法域名中,否则真机/开发者工具都会报域名不合法。- 两个 API 都支持 Promise 风格,所以可以用
await串起来,并用try/catch统一兜底;wx.saveImageToPhotosAlbum首次调用会弹出授权窗口,用户点"拒绝"后会进入catch分支,提示"保存图片失败"。 - 用户一旦拒绝过相册权限,后续再点保存会一直失败且不再弹窗,需要走授权处理流程:用
wx.getSettings(按转写稿说明,这里用回调写法更稳妥,Promise 写法在打开设置页时会出问题)读取authSetting['scope.writePhotosAlbum'],未授权时先wx.showModal引导(文案"请允许添加到相册",可设showCancel: false),用户确定后再wx.openSetting打开设置页,并在其回调里重新读取授权状态——已授权则继续调用保存,仍未授权就直接结束,不再反复打扰用户。
示例 3:访客列表(含分页触底加载)
javascript
// visitor_pkg/pages/list/index.js
Page({
data: {
visitorList: [],
isEmpty: false,
},
onLoad() {
// 获取访客列表
this.getVisitorList()
},
// 访客列表接口
async getVisitorList(current = 1, pageSize = 5) {
// 调用接口
const {
code,
data: { rows: visitorList },
} = await wx.http.get('/visitor', { current, pageSize })
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({
isEmpty: visitorList.length === 0,
visitorList,
})
},
})xml
<!-- visitor_pkg/pages/list/index.wxml -->
<block wx:if="{{!isEmpty}}">
<scroll-view
bind:scrolltolower="getMoreVisitor"
show-scrollbar="{{false}}"
enhanced
scroll-y
>
<view class="visitor">
<view class="visitor-title">我的访客</view>
<view class="visitor-list">
<van-cell-group
wx:for="{{visitorList}}"
wx:key="id"
border="{{false}}"
bind:tap="goPassport"
>
<van-cell size="large" title="{{item.houseInfo}}">
<text wx:if="{{item.status === 1}}" class="tag success">生效中</text>
<text wx:if="{{item.status === 0}}" class="tag cancel">已失效</text>
</van-cell>
<van-cell title="访客姓名" border="{{false}}" value="{{item.name}}" />
<van-cell title="手机号码" border="{{false}}" value="{{item.mobile}}" />
<van-cell title="访问日期" border="{{false}}" value="{{item.visitDate}}" />
</van-cell-group>
</view>
</view>
</scroll-view>
</block>
<view wx:else class="blank">
您还没有访客记录,请点击
<navigator hover-class="none" class="link" url="/visitor_pkg/pages/form/index">添加</navigator>
</view>要点解释与易错点:
- 列表接口返回的是分页结构,数据在
data.rows里,所以要写成data: { rows: visitorList }二次解构,直接取data会拿到整个分页对象。 - 空列表时用
isEmpty: visitorList.length === 0标记,模板里wx:if="{{!isEmpty}}"渲染scroll-view,wx:else渲染引导文案 +navigator跳转添加页,两个分支互斥,不要漏掉wx:else。 - 状态文案对应关系:
status === 1是"生效中"(success类名),status === 0是"已失效"(cancel类名),写反了样式和语义都会错。 - 分页要监听
bind:scrolltolower(滚动到底部)再请求下一页;这个事件可能在短时间内被重复触发,转写稿中明确要用节流(可用第三方库miniprogram-licia)避免重复请求。 wx:key="id"必须用列表项里真实唯一的字段,用index会导致列表更新时节点复用错乱。
常见错误
保存二维码失败,提示不支持网络路径
现象:点击“保存到本地”后无图片或直接报错。
原因:把passport.url这类网络地址直接传给wx.saveImageToPhotosAlbum。
修法:先用wx.getImageInfo取到本地临时path,再保存;整个流程加try/catch并给失败提示。真机或工具提示 download 域名不合法
现象:wx.getImageInfo调用失败,保存流程中断。
原因:小程序后台未配置downloadFile合法域名,或工具项目配置未刷新。
修法:在开发设置中补充图片资源域名,回到开发者工具刷新项目配置并重新编译。首次拒绝相册授权后,再点保存一直无反应
现象:用户误点“拒绝”后,保存按钮像失效一样。
原因:只调用保存 API,没有检测授权状态,也没有拒绝后的引导与复查。
修法:先检测相册授权;拒绝时弹窗说明并引导openSetting;从设置返回后复查,仍拒绝则停止打扰。分享页仍然出现“分享给朋友”,或“保存到本地”被一起隐藏
现象:分享打开的页面还能二次分享,或保存入口消失。
原因:显隐条件写反,或用整块toolbar控制,误伤保存按钮。
修法:按encryptedData语义只控制分享按钮;保存按钮按业务需要保留,条件写清楚并分别验证两种入口。访客列表状态标签显示反了
现象:生效中显示成“已失效”,已失效显示成“生效中”。
原因:接口状态值与模板中的status === 0/1、success/cancel映射不一致。
修法:以接口文档为准统一状态常量或映射函数,模板只消费映射后的结果。滚动触底时分页接口被重复请求,列表出现重复数据
现象:快速滑动时连续发请求,列表重复或页码错乱。
原因:scrolltolower短时间重复触发,且没有请求锁、节流和“无更多”保护。
修法:加节流与loading锁;新数据追加而非覆盖;无更多数据时停止请求。Token 过期刷新进入死循环
现象:网络面板不断请求刷新接口,页面卡死或应用崩溃。
原因:refresh token也返回 401 时,仍继续走刷新逻辑。
修法:在刷新前判断当前请求是否为刷新接口;若是且仍 401,清理本地 token 并重定向登录,终止刷新。刷新 Token 后重发请求,页面仍然空白
现象:刷新接口成功,但原业务数据没有渲染。
原因:重发时未覆盖原请求的鉴权头,或没有return重发请求的结果。
修法:基于原配置替换Authorization为新 token,并return重发请求,让业务层拿到最终数据。
练习
补全保存二维码的授权分支
要求:已授权时直接保存;首次拒绝时弹窗说明并引导打开设置;设置返回后复查;仍拒绝则不再重复打扰。写出关键伪代码或流程说明。修正分享页工具栏显隐
要求:普通登录入口显示“分享给朋友”和“保存到本地”;通过分享链接打开的页面隐藏“分享给朋友”,但保留“保存到本地”。写出 WXML 条件并说明判断依据。实现访客列表分页防抖与追加
要求:监听scrolltolower,加入节流、请求锁、页码递增、数据追加、空态和无更多提示;验证快速连续滑动不会重复请求。验证无感刷新的两条路径
要求:先模拟 access token 失效、refresh token 正常,确认自动刷新并重发;再模拟两者都失效,确认不进入死循环,清理 token 并跳转登录,登录后能回跳原页面。检查访客详情二维码的过期态
要求:分别构造validTime > 0和validTime === -1,验证倒计时、失效蒙层、保存按钮和分享入口是否符合业务预期,并记录异常点。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 访客详情-小程序保存图片 | 12:10 |
| 2 | 访客详情-授权API处理流程 | 17:12 |
| 3 | 无感刷新-过期token续期 | 16:57 |
| 4 | 无感刷新-重发请求 | 08:35 |
| 5 | 无感刷新-完善刷新token过期情况 | 12:05 |
小结
- 访客详情必须区分“登录态按 ID 获取”和“分享态按 encryptedData 获取”两条数据通道,页面初始化要按参数分流。
- 二维码保存不是直接写网络图片,而是“先取本地临时路径,再写相册”,域名配置和授权处理缺一不可。
- 授权流程的重点不是弹窗本身,而是拒绝后的引导、设置返回后的复查,以及用户仍拒绝时停止打扰。
- 分享页的权限边界要落到 UI 上:不允许二次分享时只隐藏分享入口,不能误伤保存等其他操作。
- 列表分页要同时处理触底频率、请求锁、数据追加、空态和无更多态,否则很容易重复请求或重复渲染。
- 无感刷新必须同时覆盖“刷新成功重发”和“刷新失败终止”两条路径,尤其要防止 401 递归。
- 重发请求要复用原配置并替换鉴权头,且返回重发结果,否则业务层拿不到最终数据。
- 网络请求、图片读取、相册保存和授权相关能力都要有失败兜底,避免用户侧静默失败。