You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Sanity中为文档实现递增属性?解决订单deliveryNumber冲突

Sanity中实现递增deliveryNumber的方案

一、Sanity有没有内置支持?

很明确:没有。Sanity的核心字段体系里没有全局递增序列的内置功能——毕竟它是面向文档的分布式架构,默认依赖的是随机/基于slug的_id,而非强一致性的自增数值。

二、怎么实现?还要避免竞态冲突!

直接读取最后一个订单的deliveryNumber再加1的做法在高并发场景下肯定会翻车(两个请求同时读到同一个最大值,然后生成重复的配送号)。可靠的做法是用独立计数器文档+事务性原子更新来解决,具体步骤如下:

1. 先创建一个计数器文档的Schema

在你的Sanity项目里新增一个专门用来维护配送号最大值的文档类型,比如叫deliveryCounter:

// schemas/deliveryCounter.js
export default {
  name: 'deliveryCounter',
  title: '配送号计数器',
  type: 'document',
  fields: [
    {
      name: 'currentNumber',
      title: '当前最大配送号',
      type: 'number',
      validation: Rule => Rule.required().min(0)
    }
  ],
  preview: {
    prepare() {
      return { title: '配送号全局计数器' }
    }
  }
}

然后去Sanity Studio里手动创建一个这个类型的文档,把currentNumber初始值设为0(或者你现有订单里的最大配送号,避免重复)。

2. 用事务+版本校验实现原子递增

当创建新订单时,我们要把「更新计数器」和「创建订单」放在同一个事务里,同时用版本校验避免并发冲突。用Sanity的JS客户端示例代码如下:

import { createClient } from '@sanity/client'

const client = createClient({
  projectId: '你的项目ID',
  dataset: '你的数据集',
  token: '具有写入权限的token',
  useCdn: false // 写入操作不能用CDN
})

async function createOrderWithDeliveryNumber(orderData) {
  let retryCount = 0
  const maxRetries = 3 // 遇到并发冲突时重试3次足够了

  while (retryCount < maxRetries) {
    try {
      // 第一步:获取当前计数器的最新状态
      const counter = await client.fetch('*[_type == "deliveryCounter"][0]')
      const nextDeliveryNumber = counter.currentNumber + 1

      // 第二步:执行事务,确保两个操作要么都成,要么都败
      await client.transaction()
        // 条件更新:只有计数器的版本和我们读取到的一致时才允许更新
        .patch(counter._id, {
          set: { currentNumber: nextDeliveryNumber }
        }, {
          ifRevisionID: counter._rev // 核心:避免并发覆盖
        })
        // 创建新订单,用刚生成的配送号
        .create({
          _type: 'order',
          deliveryNumber: nextDeliveryNumber,
          ...orderData // 你的其他订单字段,比如用户信息、商品列表等
        })
        .commit()

      console.log(`订单创建成功,配送号:${nextDeliveryNumber}`)
      return nextDeliveryNumber
    } catch (error) {
      if (error.message.includes('revision mismatch')) {
        // 说明有其他请求同时更新了计数器,重试即可
        retryCount++
        console.log(`遇到并发冲突,正在重试第${retryCount}次`)
        continue
      }
      // 其他错误直接抛出,比如权限问题、网络问题
      throw error
    }
  }

  throw new Error('重试次数过多,暂时无法创建订单,请稍后再试')
}

3. 核心避坑点

  • 事务绑定:把计数器更新和订单创建绑在一个事务里,不会出现“计数器加了但订单没创建”的情况,保证数据一致性。
  • 版本校验:ifRevisionID参数是关键——如果有另一个请求已经更新了计数器,当前请求的更新会失败,我们通过重试来处理这种临时冲突。
  • 重试机制:并发冲突是小概率事件,重试3次基本能解决大部分情况,不用过度重试。

4. 更简洁的备选:用Increment原子操作

Sanity的Patch API支持inc原子操作,不用手动计算数值,直接让服务器帮你加1:

async function getNextDeliveryNumber() {
  const counter = await client.fetch('*[_type == "deliveryCounter"][0]')
  // 原子递增计数器,返回更新后的文档
  const updatedCounter = await client.patch(counter._id)
    .inc({ currentNumber: 1 })
    .commit()
  return updatedCounter.currentNumber
}

// 创建订单时调用这个方法获取配送号
async function createOrder(orderData) {
  const deliveryNumber = await getNextDeliveryNumber()
  return client.create({
    _type: 'order',
    deliveryNumber,
    ...orderData
  })
}

不过这个方法的小缺点是:如果递增计数器后创建订单失败,会导致配送号“跳号”。所以如果你的业务不允许跳号,还是优先用事务绑定的方案。

三、额外提醒

  • 权限控制:一定要限制计数器文档的修改权限,只让后台服务或者授权的客户端能操作,防止恶意篡改。
  • 初始值校准:如果是已有订单的老系统迁移,先统计现有订单的最大deliveryNumber,把计数器的初始值设为这个数,避免重复。
  • 错误反馈:给用户清晰的错误提示,比如重试失败时告诉用户“暂时无法创建订单,请稍后再试”,不要让用户摸不着头脑。

内容的提问来源于stack exchange,提问作者ArneHugo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:48:59