主题
购物车与购物车登录的判断(Day5 2/2)
本页对应 day5,共 6 段视频,约 2 小时 27 分。
学习目标
- 能够根据路由参数区分“立即购买”和“购物车”两种订单来源,并分别完成订单商品信息、祝福语和总价的渲染。
- 能够调用收货地址接口获取当前登录用户的默认收货地址,并在订单详情页正确渲染收货人、手机号、详细地址及地址标签。
- 能够收集并校验提交订单所需参数,包括收货地址 ID、订购人姓名与手机号、期望送达日期、祝福语和商品列表。
- 能够按“提交订单获取订单号→根据订单号获取支付参数→调用微信支付 API”的顺序说明并实现微信小程序支付流程。
- 能够在支付成功回调中查询后台支付结果,并根据结果跳转支付成功页或进行相应后续处理。
- 能够说明微信小程序支付测试对账号权限的要求,并正确使用企业开发者账号、开发者微信号和 AppID 完成测试配置。
知识点
2.1 获取购物车数据并渲染:先分清“从哪来”,再决定“取什么”
为什么需要:购物车页和订单页最容易混的一点,是数据来源不同。从购物车结算进入订单页时,不需要再带某个商品 ID,因为购物车接口只要带 token 就能把当前用户的购物车数据整体返回;而从商品详情点“立即购买”进入订单页时,才需要携带 goods_id 和祝福语 remark,再根据商品 ID 拿详情。这个判断依据在转写里说得很清楚:看 options 里有没有 remark 或商品 ID,有就按立即购买处理,没有就按购物车处理。
怎么想:先把购物车数据当成一个数组 cartArr 存到页面 data 里,再渲染商品列表。购物车接口通常返回商品项,字段按你项目接口为准,常见会用到 goods_id、图片、名称、价格、数量、勾选状态。转写中反复强调“从购物车来不需要带商品 ID,只要 token”,所以不要把立即购买那套参数硬套到购物车。
怎么做:页面加载时调用获取购物车方法,拿到 result.data 后 setData。模板里循环 cartArr 渲染。总价如果接口没直接返回,就前端用 reduce 算;转写中明确提到“购物车数据是不是得自己算”,并给出“上一项 + 当前商品数量 × 当前商品价格”的思路。
js
// 结构示意:接口路径与字段以你项目为准
async getCartList() {
const res = await reqCartList() // 需带 token
if (res.code === 200) {
this.setData({ cartArr: res.data })
this.calcTotal()
}
},
calcTotal() {
const total = this.data.cartArr.reduce((prev, item) => {
return prev + item.price * item.count
}, 0)
this.setData({ total })
}html
<!-- 字段名按接口返回调整 -->
<view wx:for="{{cartArr}}" wx:key="goods_id">
<image src="{{item.image}}" />
<text>{{item.name}}</text>
<text>¥{{item.price}}</text>
<text>数量:{{item.count}}</text>
</view>
<view>合计:¥{{total}}</view>易错点:一是把“立即购买”和“购物车”的参数混用;二是接口返回结构没看清,转写里专门提醒“后台想返回对象还是数组要看接口”,不要凭感觉写;三是总价字段接口不一定给,给了就直接用,没给再前端算,别重复加。
2.2 修改商品勾选状态:先本地反馈,再和服务端对齐
为什么需要:购物车结算只应结算勾选商品,所以勾选状态必须可修改。转写中算总价时提到“他勾没勾”,说明勾选状态会影响业务判断。
怎么想:点击勾选后,先明确当前商品标识 goods_id,再决定是调“修改勾选”接口,还是只在前端维护。教程里不要猜接口名,按项目 API 来。比较稳的做法是:点击后更新本地 checked,同时发请求;失败则回滚并提示。
怎么做:给勾选框绑定点击事件,传 goods_id;在方法里找到对应项,取反 checked,setData 后调接口。若接口要求传 checked 或 selected,以文档为准。
易错点:不要直接改 this.data.cartArr[i].checked 就以为页面会更新,必须 setData;不要只改前端不通知服务端,否则刷新后状态丢失;批量全选要区分“全选/反选”。
2.3 删除商品:确认、定位、更新、重算
为什么需要:购物车是临时决策区,删除是基本操作。
怎么想:删除前先确认商品标识,不要用数组下标当唯一 ID,因为删除后下标会变。转写里多次出现 goods_id,应优先用它定位。
怎么做:点击删除 → 弹确认框 → 调删除接口 → 成功后从 cartArr 过滤掉该项 → 重新计算总价。若接口失败,保留原数据并提示。
js
async delCart(goodsId) {
const confirm = await wx.showModal({ title: '提示', content: '确认删除?' })
if (!confirm.confirm) return
const res = await reqDelCart(goodsId) // 接口以项目为准
if (res.code === 200) {
const cartArr = this.data.cartArr.filter(item => item.goods_id !== goodsId)
this.setData({ cartArr })
this.calcTotal()
}
}易错点:用 index 删除导致删错;删除后忘记重算总价;不处理接口失败。
2.4 修改数量:边界、请求、防抖节流
为什么需要:数量影响总价和库存,必须有加减控制。转写里提到“防抖:类似于回城;节流:英雄的 CD”,可以这样理解:防抖是连续点击只认最后一次,像回城被打断后重新读条;节流是固定时间内只触发一次,像技能 CD。
怎么想:点“+”不能超过库存,点“-”不能小于 1;连续快速点击时,不要每次都发请求,可用防抖或节流。数量修改成功后重算总价。
怎么做:先本地改数量并重算,再发请求;失败回滚。若项目要求每次点击都同步服务端,至少加节流,避免请求风暴。
易错点:数量为 0 还允许减;库存上限没判断;只改数量不重算总价;防抖节流用错场景——数量连续点更适合防抖/节流保护请求,而不是把最后一次操作丢掉不处理。
补充:微信小程序里不要直接用 window
笔记里特别提醒:微信小程序插件/小程序环境使用不了 window。不要把 Web 项目里的 window.xxx 直接搬过来,应使用小程序页面实例 this、wx API 或项目封装。原笔记链接保留如下:
易错点:从 Web 复制代码时残留 window.location、window.localStorage 等;小程序里应改用 wx.navigateTo、wx.setStorageSync 等对应能力。
本节要判断什么:登录态与入口来源
这节标题写的是“购物车登录的判断”,但结合转写稿,实际落点在订单详情页:进入订单页之前要先确认登录态,进入之后还要判断用户是从“商品详情—立即购买”来的,还是从“购物车—结算”来的。为什么需要?因为两种入口展示的数据不一样:立即购买只展示当前一个商品,购物车结算要展示购物车里的商品列表;如果不判断,页面就会拿错数据。怎么想?看路由跳转时带了什么参数:立即购买必须带商品 ID 和祝福语(订单备注 remark),购物车结算不需要带商品 ID,只需要登录 token 去拉购物车列表。怎么做?在订单页 onLoad(options) 里保存参数,再分情况发请求。易错点:转写里的“祝福语/住户语/朱福语”都是订单备注 remark,“故子ID/故事ID/裤子ID”都是商品 ID goodsId,写代码时统一用接口文档的字段名,别自己造名字。
用路由参数区分立即购买与购物车
为什么需要?订单详情页一进来就要决定调哪个接口。怎么想?转写里反复说:“有祝福语或者有商品 ID,就一定是立即购买;没有,就是购物车。”因为购物车结算时不需要商品 ID,接口只要 token 就能返回全部购物车数据。怎么做?可以先打印 options 验证:
js
onLoad(options) {
console.log(options)
this.goodsId = options.goodsId
this.remark = options.remark
if (options.remark || options.goodsId) {
this.getGoodsInfo()
} else {
this.getCartList()
}
}易错点:判断依据要稳定。转写里说“根据他或者他都行”,但实际调试时先看路由参数有没有带过来;如果 remark 为空字符串,单靠它可能误判,所以商品 ID 也要一起看。另一个易错点是请求顺序:立即购买的商品详情请求要放在来源判断之后,不能先请求再判断。
立即购买:跳转带 goodsId 和 remark
为什么需要?立即购买页要展示当前商品详情,还要把用户填的祝福语带到订单页。怎么想?从商品详情页点“确定/立即购买”时,路由跳转的 query 里带两个值:goodsId 和 remark。怎么做?转写里的做法是:
js
wx.navigateTo({
url: `/pages/order/order?goodsId=${goodsId}&remark=${remark}`
})订单页拿到后,调“立即购买”接口,按接口文档带商品 ID 和祝福语参数;转写里提到商品 ID 在路径上,祝福语通过 query 带。易错点:不要漏 token;不要用购物车接口去拿立即购买商品;调试时先刷新页面看 options 是否真有值,再看接口返回是不是一个商品对象。
购物车入口:拉列表并自己算总价
为什么需要?购物车结算要展示所有商品,接口返回的是数组。怎么想?转写里调试时打印 result,确认返回的是数组,再存到 cartList 渲染。怎么做?列表项渲染 item.goodsId、图片、名称、价格、数量;总价接口不一定返回,转写里明确要自己算:
js
this.total = this.cartList.reduce((prev, item) => {
return prev + item.price * item.count
}, 0)易错点:转写里说“都要支付了,还判断勾不勾吗,不用”,所以这里按结算数据直接算;数量字段要对准接口,别把计数器字段写错;总价算完要赋值到页面数据再刷新验证。
收货地址与提交订单参数
为什么需要?不管从哪个入口来,订单页都要有收货人地址。怎么想?转写里说地址请求不用区分来源,进页面就发;地址接口要带 token,因为每个账号的地址不同。怎么做?onLoad 里调地址接口,存 userInfo,渲染完整地址、标签、收货人、手机号。提交订单时收集:
js
const params = {
userAddressId: this.userInfo.id,
buyName: this.buyName,
buyPhone: this.buyPhone,
expectDate: this.expectDate,
remark: this.remark,
cartList: this.cartList
}易错点:提交订单是 POST,必须带 token;期望送达日期要转成年月日,转写里用 moment 处理时间戳;地址为空先去地址管理添加,别在订单页硬编造。
支付流程与登录判断的收尾
为什么需要?提交订单后拿到订单号,还要拿支付参数才能调 wx.requestPayment。怎么想?转写里的流程是:提交订单拿订单号→根据订单号拿支付参数→调微信支付→支付成功后查后台支付结果→跳成功页。怎么做?
js
wx.requestPayment({
timeStamp,
nonceStr,
package,
signType,
paySign,
success() {
// 再查后台支付结果
}
})易错点:模拟器测试会弹二维码,真机支付只有企业开发人员账号能测;管理员把你的微信号和开发者 AppID 加进去后,要退出登录再重登才生效;success 只代表支付动作成功,严谨做法还要问后台“钱到没到公司账”,再决定是否跳支付成功页。
课堂画面






代码示例
示例一:订单详情页 onLoad —— 区分「立即购买 / 购物车」并计算总价
html
<!-- pages/order/order.wxml -->
<view class="order-page">
<!-- 祝福语:从立即购买带过来的 remark,有值才展示 -->
<view class="blessing" wx:if="{{remark}}">祝福语:{{remark}}</view>
<!-- 商品列表:购物车过来是多条,立即购买只有一条 -->
<view class="goods-item" wx:for="{{cartArr}}" wx:key="goodsId">
<image class="goods-img" src="{{item.goodsImg}}"></image>
<view class="goods-name">{{item.goodsName}}</view>
<view class="goods-price">¥{{item.price}}</view>
<view class="goods-count">x{{item.count}}</view>
</view>
<view class="total-bar">合计:¥{{total}}</view>
</view>js
// pages/order/order.js
import { reqCartList, reqImmediateBuy, reqAddress } from '../../api/order'
Page({
data: {
cartArr: [], // 商品列表
total: 0, // 商品总价
remark: '', // 祝福语
goodsId: '', // 立即购买的商品ID
userInfo: {} // 收货人地址
},
onLoad(options) {
// 判断依据:立即购买一定带 remark / goodsId,购物车一定不带
if (options.remark) {
this.setData({
goodsId: options.goodsId,
remark: options.remark
})
this.getImmediateBuy() // 立即购买:拿当前这一个商品的详情
} else {
this.getCartList() // 购物车:拿全部购物车数据
}
// 不管从哪来,收货人地址都要拿
this.getAddress()
},
// 购物车过来的数据
async getCartList() {
const result = await reqCartList()
if (result.code === 200) {
this.setData({ cartArr: result.data })
this.computeTotal()
}
},
// 立即购买过来的数据
async getImmediateBuy() {
const { goodsId, remark } = this.data
const result = await reqImmediateBuy(goodsId, remark)
if (result.code === 200) {
this.setData({
cartArr: result.data.cartList,
total: result.data.total
})
}
},
// 计算购物车商品总价
computeTotal() {
const total = this.data.cartArr.reduce((prev, item) => {
return prev + item.count * item.price
}, 0)
this.setData({ total })
}
})关键行与易错点
if (options.remark)是区分两种入口的唯一依据:立即购买跳转时一定带上goodsId和remark,购物车跳转时什么都不带。reduce第二个参数0是初始值,不写会报错(空数组时reduce无初始值直接抛异常)。- 计算总价必须放在
setData({ cartArr })之后:购物车接口返回的数据里没有总价,只能拿数据后再算。 - 两个请求是「二选一」,不要写成 if/else 都执行;
getAddress则与来源无关,放在 if 外面。
示例二:提交订单 → 拿订单号 → 拿支付参数 → 发起微信支付 → 查询支付结果
js
// pages/order/order.js 继续
import { reqSubmitOrder, reqPayInfo, reqPayStatus } from '../../api/order'
Page({
// ...省略 data 与 onLoad
// 结算按钮:先整理参数,再提交订单
async submitOrder() {
const { userInfo, buyName, buyPhone, deliveryDate, remark, cartArr } = this.data
const params = {
userAddressId: userInfo.id, // 收货人地址ID
buyName, // 订购人姓名
buyPhone, // 订购人手机号
deliveryDate, // 期望送达日期
remark, // 祝福语
cartList: cartArr // 商品信息
}
// 注意:提交订单是 POST
const result = await reqSubmitOrder(params)
if (result.code === 200) {
const orderNo = result.data // 后台返回的订单号
this.getPayInfo(orderNo) // 拿到订单号立刻去要支付参数
}
},
// 拿支付参数并调起微信支付
async getPayInfo(orderNo) {
const result = await reqPayInfo(orderNo)
if (result.code === 200) {
const payParams = result.data
wx.requestPayment({
timeStamp: payParams.timeStamp, // 必填
nonceStr: payParams.nonceStr, // 必填
package: payParams.package, // 必填
signType: payParams.signType, // 可选(MD5)
paySign: payParams.paySign, // 必填
success: () => {
// 支付成功后先问后台:钱到底到没到账
this.queryPayStatus(orderNo)
},
fail: () => {
wx.showToast({ title: '支付已取消', icon: 'none' })
}
})
}
},
// 查询支付结果,确认成功再跳支付成功页
async queryPayStatus(orderNo) {
const result = await reqPayStatus(orderNo)
if (result.code === 200) {
wx.navigateTo({ url: '/pages/pay/success/success' })
}
}
})关键行与易错点
- 三个请求的顺序不能乱:提交订单拿订单号 → 拿支付参数 →
wx.requestPayment→ 查支付结果。没有订单号拿不到支付参数,没有支付参数调不起支付。 wx.requestPayment里timeStamp / nonceStr / package / paySign四个必填,signType可有可无;参数名大小写必须和文档一致,写错直接报「参数错误」。success回调只代表「用户付了钱」,不代表「钱到了商家账」,所以还要调queryPayStatus查后台,这是严谨写法。- 提交订单必须是 POST(
method: 'post'),写成 get 接口一直失败。 - 支付成功页若不是 tabBar 页面才能用
wx.navigateTo;回首页 tabBar 页要用wx.switchTab。
示例三:收货人地址渲染 + 期望送达日期(弹窗 + 日历 + moment 格式化)
html
<!-- pages/order/order.wxml -->
<view class="address" bindtap="toAddressList">
<view class="address-tag">{{userInfo.tagName}}</view>
<view class="address-detail">{{userInfo.fullAddress}}</view>
<view class="address-user">{{userInfo.name}} {{userInfo.phone}}</view>
</view>
<view class="cell" bindtap="showPop">
<text>期望送达日期</text>
<text class="cell-value">{{deliveryDate || '请选择'}}</text>
</view>
<van-popup show="{{isShowPop}}" position="bottom" bind:close="closePop">
<van-calendar
min-date="{{minDate}}"
bind:confirm="confirmDate"
bind:cancel="closePop"
/>
</van-popup>js
// pages/order/order.js 继续
import moment from 'moment'
Page({
// ...省略 data 与 onLoad
// 获取收货人地址(token 走请求头,接口本身无参数)
async getAddress() {
const result = await reqAddress()
if (result.code === 200) {
this.setData({ userInfo: result.data })
}
},
// 点击「期望送达日期」,弹出日历
showPop() {
this.setData({
isShowPop: true,
minDate: Date.now() // 最小可选时间 = 当前时间
})
},
// 遮罩层与取消按钮共用:只关弹窗,不收集数据
closePop() {
this.setData({ isShowPop: false })
},
// 日历确定:回调参数是时间戳,要格式化成年月日
confirmDate(e) {
this.setData({
deliveryDate: moment(e.detail).format('YYYY-MM-DD'),
isShowPop: false
})
}
})关键行与易错点
min-date传的是时间戳,不是字符串;不传就默认一年前,用户能选到十年前。- 日历
confirm回调的e.detail是时间戳,必须用moment(e.detail).format('YYYY-MM-DD')转成YYYY-MM-DD再存,否则提交订单时日期字段格式不对。 confirmDate里除了存日期,一定要把isShowPop置为false关弹窗;取消/遮罩关闭不要写收集数据的逻辑。- 收货人地址接口必须带 token(请求头里),因为拿的是「当前登录账号」的地址;接口返回的是对象不是数组。
- 装完
moment记得重新构建 npm,构建后还报错就清缓存重下;也可以用dayjs替代。
常见错误
从购物车进订单页,页面空白或残留上一个商品的数据
- 现象:点击购物车结算进入订单页,商品列表不渲染,或显示的是刚才"立即购买"那件商品。
- 原因:来源判断的字段选错(用 goodsId 判),或两条分支的请求写在同一层级、后发的请求把先存的数据覆盖了。
- 修法:固定用
options.remark(或 goodsId)是否存在来分阵营;购物车分支只调购物车接口,立即购买分支只调详情接口,各自await完再赋值到data,不要交叉赋值。
立即购买的商品信息渲染不出来
- 现象:祝福语能显示,商品名/图/价是空的。
- 原因:获取立即购买商品详情的请求写进了
else分支(那是购物车的分支),或者写了但结果没存进data,只console.log了一下。 - 修法:把请求放到
if (options.remark)分支内,const res = await ...后this.setData({ goodsInfo: res.data }),模板里字段名与接口返回字段逐字对齐。
点结算提交订单,后端报参数缺失或 400
- 现象:结算按钮点了没反应,或接口返回参数错误。
- 原因:请求方法写成了 GET(提交订单必须是 POST);或参数字段名与文档不一致(如
userAddressId / buyerName / buyerPhone / expectDate / remark / cartList大小写、拼写错);或某个参数在 data 里根本没收集到。 - 修法:先按文档把 method 改成 POST;发请求前把整理好的参数对象
console.log出来,与文档字段表逐项对照;缺的字段回到模板补model:value绑定。
wx.requestPayment调不起来 / 报参数不合法- 现象:拿到订单号就调支付,控制台报缺少参数。
- 原因:订单号不等于支付参数。订单号只是"凭证",还得再向后台要一次支付参数;且
timeStamp / nonceStr / package / signType / paySign这五个名字必须一字不差。 - 修法:提交订单拿到订单号 → 立刻再发一次请求换支付参数 → 把返回对象整体展开传给
wx.requestPayment,不要自己重命名 key。
支付成功后页面停在订单页不动
- 现象:输入密码付完款,点"完成"回到的还是订单详情页。
- 原因:
success回调里没写后续逻辑;或直接拿success当"钱已到账",没向服务端确认。 - 修法:
success里再发一次"查询支付结果"请求,code === 200才跳转;跳转目标若是 tabBar 页(如首页),必须用wx.reLaunch,navigateTo会失败。
期望送达日期显示成一串数字
- 现象:选了日期,页面上回显的是
1694xxxxxxx。 - 原因:直接把 picker 回调的
e.detail赋值给了 data,没有格式化。 - 修法:确认取值是
e.detail(不是e),再用 moment/dayjs 转成YYYY-MM-DD后存 data 并回显,同时把弹窗状态置回 false。
- 现象:选了日期,页面上回显的是
真机扫码提示"请用开发者本人微信扫码"
- 现象:模拟器能弹出二维码,真机扫了付不了款。
- 原因:用的是个人小程序账号,或你的微信号没被加进该企业小程序的开发者列表。
- 修法:确认登录的是企业账号;把你的微信号 + 开发者 AppID 发给管理员添加,加完后退出登录再重新登录才会生效。
练习
在订单页
onLoad里用options.remark的存在性做来源分流,两个分支各打一条日志,分别从"商品详情立即购买"和"购物车结算"进入,验证走的分支正确、且cartList/goodsInfo只有一个被赋值。给"提交订单"做一次参数体检:在发请求前
console.log出完整参数对象,对照接口文档字段表逐项打勾,把缺的字段在模板里补齐model:value绑定(含订购人姓名、手机号、期望日期、备注)。实现期望送达日期:点击单元格弹出层 → 日历选日期 → 确定后格式化回显并自动关闭弹窗;取消或点遮罩也要关闭。完成后连续操作三次,确认不会出现"关不掉"或"值不更新"。
给支付流程补上"二次确认":在
wx.requestPayment的success里再查一次支付结果,成功跳支付成功页,失败给出提示且停留在原页。用代码走查方式说明为什么不能只信success。边界处理:用一个新的空账号登录,进入订单页观察收货地址区域的表现;说明接口返回为空时页面应该怎么兜底(提示去添加地址),并顺手把地址接口返回结构(对象还是数组)打印确认。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 订单详情页的数据渲染 | 31:34 |
| 2 | 收货人地址数据的渲染 | 12:00 |
| 3 | 微信支付的流程 | 32:43 |
| 4 | 提交订单数据的收集 | 27:42 |
| 5 | 微信小程序支付的流程 | 35:35 |
| 6 | 微信开发者测试账号注意 | 07:24 |
小结
- 订单详情页本质是"一个页面、两种数据源",来源判断做对,后面全是数据渲染;判断依据要选最稳定的字段,不要凭猜测。
- 两个数据源的请求必须分支隔离、各自串行,谁的数据存谁的变量,最忌讳"都发、都存、互相覆盖"。
- 支付这条链路上前端只碰三件事:拿订单号、换支付参数、查支付结果;中间所有的加密、签名、生成预订单都是后台的事。
- 前端唯一能调用的支付能力就是
wx.requestPayment这一个 API,弹窗、验密、扣款全部由微信自己完成,别想着去接管。 success只代表"这次交互走完了",不代表"钱到公司账了",涉及钱的流程一定要向服务端二次确认,这是严谨性问题。- 参数字段名必须与文档逐字对齐,大小写、拼写、请求方法(POST)三样错一样都调不通,写之前先抄一遍文档。
- 开发阶段支付有硬权限门槛:企业账号 + 管理员把你加成开发者 + 退登重进,这三步少一步都测不了,提前跟管理员沟通好,别等到联调才发现。
- 这类业务型页面的通用排查顺序:先看来源判断对不对,再看请求发没发、参数全不全,最后看数据存没存、模板字段对不对得上。