Skip to content

购物车与购物车登录的判断(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 或项目封装。原笔记链接保留如下:

微信小程序插件中 window 不可用说明

易错点:从 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 只代表支付动作成功,严谨做法还要问后台“钱到没到公司账”,再决定是否跳支付成功页。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:订单详情页 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 替代。

常见错误 ​

  1. 从购物车进订单页,页面空白或残留上一个商品的数据

    • 现象:点击购物车结算进入订单页,商品列表不渲染,或显示的是刚才"立即购买"那件商品。
    • 原因:来源判断的字段选错(用 goodsId 判),或两条分支的请求写在同一层级、后发的请求把先存的数据覆盖了。
    • 修法:固定用 options.remark(或 goodsId)是否存在来分阵营;购物车分支只调购物车接口,立即购买分支只调详情接口,各自 await 完再赋值到 data,不要交叉赋值。
  2. 立即购买的商品信息渲染不出来

    • 现象:祝福语能显示,商品名/图/价是空的。
    • 原因:获取立即购买商品详情的请求写进了 else 分支(那是购物车的分支),或者写了但结果没存进 data,只 console.log 了一下。
    • 修法:把请求放到 if (options.remark) 分支内,const res = await ... 后 this.setData({ goodsInfo: res.data }),模板里字段名与接口返回字段逐字对齐。
  3. 点结算提交订单,后端报参数缺失或 400

    • 现象:结算按钮点了没反应,或接口返回参数错误。
    • 原因:请求方法写成了 GET(提交订单必须是 POST);或参数字段名与文档不一致(如 userAddressId / buyerName / buyerPhone / expectDate / remark / cartList 大小写、拼写错);或某个参数在 data 里根本没收集到。
    • 修法:先按文档把 method 改成 POST;发请求前把整理好的参数对象 console.log 出来,与文档字段表逐项对照;缺的字段回到模板补 model:value 绑定。
  4. wx.requestPayment 调不起来 / 报参数不合法

    • 现象:拿到订单号就调支付,控制台报缺少参数。
    • 原因:订单号不等于支付参数。订单号只是"凭证",还得再向后台要一次支付参数;且 timeStamp / nonceStr / package / signType / paySign 这五个名字必须一字不差。
    • 修法:提交订单拿到订单号 → 立刻再发一次请求换支付参数 → 把返回对象整体展开传给 wx.requestPayment,不要自己重命名 key。
  5. 支付成功后页面停在订单页不动

    • 现象:输入密码付完款,点"完成"回到的还是订单详情页。
    • 原因:success 回调里没写后续逻辑;或直接拿 success 当"钱已到账",没向服务端确认。
    • 修法:success 里再发一次"查询支付结果"请求,code === 200 才跳转;跳转目标若是 tabBar 页(如首页),必须用 wx.reLaunch,navigateTo 会失败。
  6. 期望送达日期显示成一串数字

    • 现象:选了日期,页面上回显的是 1694xxxxxxx。
    • 原因:直接把 picker 回调的 e.detail 赋值给了 data,没有格式化。
    • 修法:确认取值是 e.detail(不是 e),再用 moment/dayjs 转成 YYYY-MM-DD 后存 data 并回显,同时把弹窗状态置回 false。
  7. 真机扫码提示"请用开发者本人微信扫码"

    • 现象:模拟器能弹出二维码,真机扫了付不了款。
    • 原因:用的是个人小程序账号,或你的微信号没被加进该企业小程序的开发者列表。
    • 修法:确认登录的是企业账号;把你的微信号 + 开发者 AppID 发给管理员添加,加完后退出登录再重新登录才会生效。

练习 ​

  1. 在订单页 onLoad 里用 options.remark 的存在性做来源分流,两个分支各打一条日志,分别从"商品详情立即购买"和"购物车结算"进入,验证走的分支正确、且 cartList / goodsInfo 只有一个被赋值。

  2. 给"提交订单"做一次参数体检:在发请求前 console.log 出完整参数对象,对照接口文档字段表逐项打勾,把缺的字段在模板里补齐 model:value 绑定(含订购人姓名、手机号、期望日期、备注)。

  3. 实现期望送达日期:点击单元格弹出层 → 日历选日期 → 确定后格式化回显并自动关闭弹窗;取消或点遮罩也要关闭。完成后连续操作三次,确认不会出现"关不掉"或"值不更新"。

  4. 给支付流程补上"二次确认":在 wx.requestPayment 的 success 里再查一次支付结果,成功跳支付成功页,失败给出提示且停留在原页。用代码走查方式说明为什么不能只信 success。

  5. 边界处理:用一个新的空账号登录,进入订单页观察收货地址区域的表现;说明接口返回为空时页面应该怎么兜底(提示去添加地址),并顺手把地址接口返回结构(对象还是数组)打印确认。

原视频对照 ​

#视频时长
1订单详情页的数据渲染31:34
2收货人地址数据的渲染12:00
3微信支付的流程32:43
4提交订单数据的收集27:42
5微信小程序支付的流程35:35
6微信开发者测试账号注意07:24

小结 ​

  1. 订单详情页本质是"一个页面、两种数据源",来源判断做对,后面全是数据渲染;判断依据要选最稳定的字段,不要凭猜测。
  2. 两个数据源的请求必须分支隔离、各自串行,谁的数据存谁的变量,最忌讳"都发、都存、互相覆盖"。
  3. 支付这条链路上前端只碰三件事:拿订单号、换支付参数、查支付结果;中间所有的加密、签名、生成预订单都是后台的事。
  4. 前端唯一能调用的支付能力就是 wx.requestPayment 这一个 API,弹窗、验密、扣款全部由微信自己完成,别想着去接管。
  5. success 只代表"这次交互走完了",不代表"钱到公司账了",涉及钱的流程一定要向服务端二次确认,这是严谨性问题。
  6. 参数字段名必须与文档逐字对齐,大小写、拼写、请求方法(POST)三样错一样都调不通,写之前先抄一遍文档。
  7. 开发阶段支付有硬权限门槛:企业账号 + 管理员把你加成开发者 + 退登重进,这三步少一步都测不了,提前跟管理员沟通好,别等到联调才发现。
  8. 这类业务型页面的通用排查顺序:先看来源判断对不对,再看请求发没发、参数全不全,最后看数据存没存、模板字段对不对得上。