Skip to content

CloudData系统快速指南 ​

概述 ​

CloudData系统是WasiCore框架中的用户数据管理核心,提供统一、类型安全、高性能的云数据操作API。本指南将帮助您快速上手CloudData系统的基本使用。

重要概念区分 ​

在开始之前,必须理解这个核心区别:

  • Player.Id (int):当局游戏的槽位编号(0,1,2...),每次游戏会话可能不同
  • User.UserId (long):用户的全局唯一标识,持久不变

玩家云数据使用 User.UserId 或 Player 对象(会自动提取 UserId)。非玩家的全局数据可以使用项目自行约定的保留 UserId。

全局数据也可以使用云数据 ​

云数据按 userId 区分逻辑数据表,不要求这个 ID 对应真实存在的平台账号。正式平台用户的 UserId 大于 10000,通常为 9 位数及以上;项目可约定 0 < userId < 10000 的数值保存非玩家的全局数据,例如用 1 保存天梯表、2 保存游戏统计、3 保存全服活动进度。

在服务端直接使用 CloudData.ForUser(reservedUserId) 写入,并用相同 ID 查询即可;无需注册账号或创建玩家对象,只需等待 CloudData.WaitUntilReadyAsync()。ID 的用途应集中定义并保持稳定。完整示例见使用保留 UserId 保存全局数据。

正确的参数类型 ​

csharp
// 正确:使用 UserId 参数
public async Task SavePlayerProgress(long userId, int level) 
{
    await CloudData.ForUser(userId).SetData("level", level).ExecuteAsync();
}

// 正确:使用 Player 对象参数(推荐用于游戏逻辑)
public async Task SavePlayerProgress(Player player, int level) 
{
    await CloudData.ForPlayer(player).SetData("level", level).ExecuteAsync();
}

// 错误:容易误导的参数名
public async Task SavePlayerProgress(long playerId, int level)  // 看起来像Player.Id!
{
    await CloudData.ForUser(playerId).SetData("level", level).ExecuteAsync();  // 易混淆
}

执行额度 ​

云数据默认每个游戏局共享每窗口 300 次查询、300 次事务提交、48 MiB 累计提交量阈值;正常 1 倍速下窗口为 60 秒。单次完整响应默认最多 32 MiB - 100 KiB。额度由同局所有玩家和业务模块共享,应批量查询、合并事务并预留余量。窗口计时受倍速和暂停影响;批量 API 计数、配置与超限处理见默认硬限制与计数规则。

5分钟快速开始 ​

1. 基础概念 ​

CloudData系统支持以下数据类型:

类型用途示例
BigInt整数数据等级、经验、积分
VarChar255字符串数据昵称、状态
Currency货币数据金币、钻石
CappedData有上限数据(可定时重置为0)体力、每日活跃度、签到进度
Blob二进制数据配置、存档
ListItem列表数据背包、好友

2. Key 命名与大小写 ​

远端云数据数据库保存和比较 key 时按大小写不敏感语义处理,因此不要用 Gold 和 gold 表示两份不同数据。WasiCore 包装层也按这个语义调整了按 key 取值的便利方法:UserData<T>.GetByKey / HasKey / GetByKeys 以及批量查询返回的 BlobData、VarCharData、BigIntData、Utf8BlobData、DoubleBlobData 字典都使用大小写不敏感比较。

仍然建议项目内统一使用小写 ASCII 和下划线命名,并集中放在常量类里,避免同一业务字段出现多种写法。

csharp
public static class CloudDataKeys
{
    public const string Level = "level";
    public const string Gold = "gold";
    public const string DailyQuestAttempts = "daily_quest_attempts";
}

var result = await QueryCurrencyAsync([userId], [CloudDataKeys.Gold]);

3. 引入命名空间 ​

csharp
using GameCore.UserCloudData;
using static GameCore.UserCloudData.CloudData; // 简化调用

4. 运行时前置条件 ​

CloudData 只能在服务端逻辑中使用。客户端 UI、纯客户端调试和本地表现层代码不能直接访问 CloudData;这些代码应通过服务端快照、消息或同步属性拿到结果。

启动阶段不要在 GameStart 后立刻无条件读取云数据。云数据服务和玩家 UserId 都可能晚于游戏脚本初始化完成,首轮存档加载推荐同时满足两个条件:

csharp
#if SERVER
using GameCore.UserCloudData;

public async Task LoadPlayerArchiveWhenReady(Player player)
{
    // 等待云数据服务入口可用;已经 ready 时会立即返回。
    await CloudData.WaitUntilReadyAsync();

    var userId = player.User?.UserId ?? 0;
    if (userId <= 0)
    {
        Game.Logger.LogWarning("玩家 {PlayerId} 尚未获得 UserId,暂不读取云存档", player.Id);
        return;
    }

    var result = await CloudData.QueryUserDataAsync(
        userIds: [userId],
        keys: ["level", "experience"]
    );

    if (!result)
    {
        Game.Logger.LogWarning("读取云存档失败: {Result}", result.Result);
    }
}
#endif

如果项目用事件驱动,也可以订阅 Game.OnUserCloudDataServiceInitialization,或在启动聚合逻辑中检查 CloudData.IsReady / Game.IsUserCloudDataServiceInitialized。这些 API 只表示云数据服务可用,不代表某个玩家已经连入或 UserId 已经有效。

5. 查询数据 ​

csharp
// 方式1: 使用 UserId(推荐用于存储的用户ID)
var result = await QueryUserDataAsync(
    userIds: [userId],
    keys: ["level", "experience", "gold"]
);

// 方式2: 使用 Player 对象(便利方法,游戏逻辑中常用)
var playerResult = await QueryPlayersDataAsync(
    players: [player],
    keys: ["level", "experience", "gold"]
);

if (playerResult.IsSuccess)
{
    var userData = playerResult.Data.First();
    var level = userData.BigIntData["level"];
    var exp = userData.BigIntData["experience"];
    var gold = userData.CurrencyData["gold"];
}

6. 基础事务操作 ​

csharp
// 方式1: 使用 UserId
var result = await ForUser(userId)
    .SetData("level", newLevel)
    .AddToData("experience", 1000)
    .AddCurrency("gold", 500)
    .WithDescription("升级奖励")
    .ExecuteAsync();

// 方式2: 使用 Player 对象(便利方法)
var playerResult = await ForPlayer(player)
    .SetData("level", newLevel)
    .AddToData("experience", 1000)
    .AddCurrency("gold", 500)
    .WithDescription("升级奖励")
    .ExecuteAsync();

7. 错误处理 ​

csharp
if (result == UserCloudDataResult.Success)
{
    Game.Logger.LogInformation("操作成功!");
}
else
{
    Game.Logger.LogError("操作失败: {Result}", result);
}

常见限额类结果:

结果推荐处理
UserCloudDataResult.TooManyReadOperations合并查询,降低轮询频率,稍后重试
UserCloudDataResult.TooManyWriteOperations合并相关事务,避免逐字段或逐奖励提交
UserCloudDataResult.MessageSizeTooLarge缩小 Blob / ListItem payload,拆分大型存档或批量数据

常见使用场景 ​

场景1: 玩家登录 ​

csharp
public async Task<PlayerLoginInfo> HandlePlayerLogin(long userId)  // 注意:这是UserId,不是Player.Id
{
    // 查询玩家基础信息
    var playerData = await QueryUserDataAsync(
        userIds: [userId],  // userId是User.UserId
        keys: ["level", "experience", "last_login", "total_playtime"]
    );

    // 查询玩家货币
    var currencyData = await QueryCurrencyAsync(
        userIds: [userId],
        keys: ["gold", "diamond", "energy"]
    );

    // 更新登录时间
    var updateResult = await ForUser(userId)
        .SetData("last_login", DateTime.UtcNow.ToString())
        .AddToData("login_count", 1)
        .WithDescription("玩家登录")
        .ExecuteAsync();

    return new PlayerLoginInfo
    {
        Level = playerData.Data.First().BigIntData["level"],
        Experience = playerData.Data.First().BigIntData["experience"],
        Gold = currencyData.Data.First().CurrencyData["gold"],
        // ... 其他数据
    };
}

场景2: 完成任务 ​

csharp
public async Task<bool> CompleteQuest(long userId, QuestInfo quest)  // 注意:这是UserId
{
    var builder = ForUser(userId);  // userId是User.UserId,不是Player.Id

    // 添加经验和金币奖励
    builder.AddCurrency("experience", quest.ExpReward)
           .AddCurrency("gold", quest.GoldReward);

    // 添加物品奖励
    if (quest.ItemRewards.Any())
    {
        var itemRefs = builder.PrepareListItems("inventory", quest.ItemRewards);
        builder.AddListItems(itemRefs);
    }

    // 更新任务状态
    builder.SetData($"quest_{quest.Id}_completed", true)
           .SetData($"quest_{quest.Id}_complete_time", DateTime.UtcNow.ToString());

    var result = await builder
        .WithDescription($"完成任务: {quest.Name}")
        .ExecuteAsync();

    return result == UserCloudDataResult.Success;
}

场景3: 商店购买 ​

csharp
public async Task<PurchaseResult> PurchaseItem(long userId, int itemId, int cost)  // UserId参数
{
    // 查询玩家当前金币
    var currentGold = await QueryCurrencyAsync(
        userIds: [userId],  // userId是User.UserId
        keys: ["gold"]
    );

    if (!currentGold.IsSuccess)
        return PurchaseResult.NetworkError;

    var goldAmount = currentGold.Data.First().CurrencyData["gold"];
    if (goldAmount < cost)
        return PurchaseResult.InsufficientFunds;

    // 执行购买
    var builder = ForUser(userId);  // 使用UserId
    var itemRef = builder.PrepareListItem("inventory", CreateItemData(itemId));

    var result = await builder
        .CostCurrency("gold", cost)        // 扣除金币
        .AddListItem(itemRef)              // 添加物品
        .SetData("last_purchase", DateTime.UtcNow.ToString())
        .WithDescription($"购买物品 {itemId}")
        .ExecuteAsync();

    if (result == UserCloudDataResult.Success)
    {
        return new PurchaseResult
        {
            Success = true,
            ItemId = itemRef.Id  // 返回生成的物品ID
        };
    }

    return PurchaseResult.TransactionFailed;
}

场景4: 批量用户操作(每日奖励) ​

csharp
public async Task DistributeDailyRewards(long[] userIds)  // 明确是UserId数组
{
    var tasks = userIds.Select(async userId =>
    {
        return await ForUser(userId)  // 使用UserId
            .AddCurrency("gold", 100)
            .AddCurrency("energy", 20)
            .SetData("last_daily_reward", DateTime.UtcNow.ToString())
            .WithDescription("每日登录奖励")
            .ExecuteAsync();
    });

    var results = await Task.WhenAll(tasks);
    
    // 统计成功和失败
    var successCount = results.Count(r => r == UserCloudDataResult.Success);
    Game.Logger.LogInformation("每日奖励发放: {SuccessCount}/{TotalCount} 成功", successCount, results.Length);
}

场景5: 体力系统管理 ​

体力系统有多种实现方式:

  • 定时重置:每日0点恢复满体力 → 使用CappedData重置机制
  • 线性回复:每分钟回复1点体力 → 使用CappedData + LastUpdateTime
  • 复杂逻辑:VIP加速、道具加速 → 使用普通数据 + 货币数据
csharp
public async Task<EnergySystemResult> ManagePlayerEnergy(long userId, EnergyAction action, int amount = 0)  // UserId参数
{
    switch (action)
    {
        case EnergyAction.Consume:
            // 消耗体力:增加已消耗量,每日0点重置为0(体力恢复满值),上限100
            var consumeResult = await ForUser(userId)  // 使用UserId
                .ModifyCappedData("energy_consumed", amount, 100, UserDataResetOption.Daily())
                .WithDescription($"消耗体力 +{amount}")
                .ExecuteAsync();
            return EnergySystemResult.FromCloudResult(consumeResult);

        case EnergyAction.UpgradeCapacity:
            // 提升体力上限(VIP特权)
            var upgradeResult = await ForUser(userId)
                .ModifyCappedData("energy_consumed", 0, 100 + amount)  // 新上限
                .WithDescription($"体力上限提升至 {100 + amount}")
                .ExecuteAsync();
            return EnergySystemResult.FromCloudResult(upgradeResult);

        case EnergyAction.Query:
            // 查询当前体力状态
            var queryResult = await QueryCappedDataAsync(
                userIds: [userId],  // 使用UserId
                keys: ["energy_consumed"]
            );
            
            if (queryResult.IsSuccess && queryResult.Data.Any())
            {
                var energyData = queryResult.Data.First();
                var energyConsumed = energyData.CappedData["energy_consumed"];
                var remainingEnergy = energyConsumed.Cap - energyConsumed.Value;  // 剩余体力
                return new EnergySystemResult
                {
                    Success = true,
                    CurrentEnergy = remainingEnergy,  // 剩余体力
                    MaxEnergy = energyConsumed.Cap,
                    NextResetTime = energyConsumed.NextResetTime
                };
            }
            return EnergySystemResult.QueryFailed;
    }
}

public enum EnergyAction
{
    Query,      // 查询当前状态
    Restore,    // 恢复体力  
    Consume,    // 消耗体力
    UpgradeCapacity  // 提升上限
}

场景6: 在游戏逻辑中正确使用Player对象 ​

csharp
// 推荐:在游戏事件处理中使用Player对象
public class GameEventHandler 
{
    // 玩家升级时保存数据
    public async Task OnPlayerLevelUp(Player player, int newLevel)
    {
        // 使用Player便利方法,自动提取User.UserId
        var result = await ForPlayer(player)
            .SetData("level", newLevel)
            .AddCurrency("skill_points", 1)
            .WithDescription($"升级到{newLevel}级")
            .ExecuteAsync();
    }
    
    // 玩家获得物品
    public async Task OnPlayerGetItem(Player player, ItemData item)
    {
        var builder = ForPlayer(player);  // 推荐:直接使用Player
        var itemRef = builder.PrepareListItem("inventory", item);
        
        await builder
            .AddListItem(itemRef)
            .SetData("last_item_time", DateTime.UtcNow.ToString())
            .ExecuteAsync();
    }
    
    // 从外部系统传入UserId的情况
    public async Task LoadUserProfile(long userId)  // 明确标注这是UserId
    {
        var userData = await QueryUserDataAsync(
            userIds: [userId],  // 这里确实需要UserId
            keys: ["level", "experience", "last_login"]
        );
        // 处理数据...
    }
}

// 批量玩家操作的正确方式
public async Task DistributeEventRewards(Player[] activePlayers)
{
    // 使用Player数组,自动过滤AI玩家
    var result = await ForPlayers(activePlayers)
        .ForAllUsers(builder => builder
            .AddCurrency("event_token", 10)
            .SetData("last_event_reward", DateTime.UtcNow.ToString())
        )
        .ExecuteAllAsync();
}

高级特性 ​

1. 事务优化 ​

csharp
// 系统会自动合并相同键的操作
var result = await ForUser(userId)  // 使用UserId
    .AddCurrency("gold", 100)
    .AddCurrency("gold", 50)      // 自动合并为 +150
    .AddCurrency("gold", -20)     // 最终结果: +130
    .SetData("level", 10)
    .SetData("level", 11)         // 自动优化为最后一个值
    .WithOptimization(true)       // 默认启用
    .ExecuteAsync();

2. 跨用户单次提交 ​

TransactionBuilder.ForUser(...) 适合一个用户内的原子事务;ForUsers(...).ExecuteAllAsync() 会为每个用户分别提交,适合允许部分成功的批量发奖。少数情况下,例如两个玩家之间的交易,需要在一次提交中同时修改两个用户的数据,任一操作失败都回滚整笔交易。

ForUsers(...) 只声明这个批次包含哪些用户,不会自动给这些用户添加操作。使用 ForAllUsers(...) 可为所有已声明用户添加相同操作;使用 ForUser(userId, ...) 可为不同用户分别添加不同操作。传给 ForUser(...) 的 userId 必须已经包含在最初的 ForUsers(...) 中,否则会抛出 ArgumentException,避免因漏写用户而静默丢失操作。

优先使用 ForUsers(...).ForUser(...).ExecuteSingleCommitAsync(...),它仍然保留链式 API,但只发送一次 commit:

csharp
public async Task<UserCloudDataResult> CommitTradeAsync(
    long buyerUserId,
    long sellerUserId,
    long price,
    string tradeId)
{
    return await CloudData.ForUsers(buyerUserId, sellerUserId)
        .ForUser(buyerUserId, builder => builder
            .CostCurrency("gold", price)
            .SetData("last_trade_id", tradeId)
        )
        .ForUser(sellerUserId, builder => builder
            .AddCurrency("gold", price)
            .SetData("last_trade_id", tradeId)
        )
        .ExecuteSingleCommitAsync($"玩家交易 {tradeId}");
}

如果链式封装不适合当前场景,也可以直接构造 TransactionOperation 列表并调用 CloudDataApi.ExecuteTransactionAsync(...)。每个 operation 都显式携带自己的 userId,整个列表作为一次事务提交:

csharp
using EngineInterface.BaseType;
using GameCore.UserCloudData;

public async Task<UserCloudDataResult> CommitTradeManuallyAsync(
    long buyerUserId,
    long sellerUserId,
    long price,
    string tradeId)
{
    var operations = new List<TransactionOperation>
    {
        TransactionOperation.CostCurrency(buyerUserId, "gold", price),
        TransactionOperation.AddCurrency(sellerUserId, "gold", price),
        TransactionOperation.SetVarChar255(buyerUserId, "last_trade_id", tradeId),
        TransactionOperation.SetVarChar255(sellerUserId, "last_trade_id", tradeId),
    };

    return await CloudDataApi.ExecuteTransactionAsync(operations, $"玩家交易 {tradeId}");
}

手动构造 operation list 时,如果需要新增 ListItem,也先通过 builder 生成 ListItemReference,再转成 operation。这样可以复用框架的列表项 UUID 生成逻辑:

csharp
var builder = CloudData.ForUser(userId);
var itemRef = builder.PrepareListItem("inventory", itemBytes);

var operations = new List<TransactionOperation>
{
    itemRef.ToListItemAddOperation(),
    TransactionOperation.SetBigInt(userId, "last_item_id", itemRef.Id),
};

var result = await CloudDataApi.ExecuteTransactionAsync(operations, "发放物品");

这类接口返回的是整笔提交的 UserCloudDataResult,不是逐用户结果。比如买家金币不足时,卖家的加金币操作也不会落库。

3. 列表项高级操作 ​

csharp
var builder = ForUser(userId);

// 批量准备物品
var lootItems = GenerateLootRewards();
var itemRefs = builder.PrepareListItems("inventory", lootItems);

// 在事务中使用新物品的ID
var firstItemId = itemRefs[0].Id;  // 在ExecuteAsync之前就能获取ID

var result = await builder
    .AddListItems(itemRefs)
    .SetData("last_loot_item_id", firstItemId)  // 记录最新物品ID
    .ExecuteAsync();

ListItem 也支持和普通 blob 类似的 UTF-8 / Double 便利写法。适合少量需要直接保存长文本日志、小数值样本的列表项;普通背包物品仍建议继续使用你自己的 byte[] / 序列化格式。

csharp
var builder = ForUser(userId);

var logRef = builder.PrepareUtf8ListItem("trade_logs", tradeJson);
var valueRef = builder.PrepareDoubleListItem("score_samples", 42.25);

var result = await builder
    .AddListItem(logRef)
    .AddListItem(valueRef)
    .ExecuteAsync();

var logs = await QueryUserListItemsAsync(userId, "trade_logs");
if (logs)
{
    foreach (var item in logs.Data)
    {
        if (item.TryGetUtf8Value(out var json))
        {
            Game.Logger.LogInformation("交易日志: {Json}", json);
        }
    }
}

4. 错误处理最佳实践 ​

csharp
public async Task<GameResult> SafeExecuteTransaction(
    long userId, 
    Func<TransactionBuilder, TransactionBuilder> buildTransaction)
{
    try
    {
        var builder = ForUser(userId);
        builder = buildTransaction(builder);
        
        var result = await builder.ExecuteAsync();
        
        if (result == UserCloudDataResult.Success)
        {
            return GameResult.Success;
        }

        // 根据错误类型返回不同结果
        return result switch
        {
            UserCloudDataResult.InsufficientCurrency => GameResult.InsufficientResources,
            UserCloudDataResult.FailedToSend => GameResult.NetworkError,
            UserCloudDataResult.TransactionCommitEmpty => GameResult.InvalidOperation,
            _ => GameResult.UnknownError
        };
    }
    catch (ArgumentException)
    {
        return GameResult.InvalidArguments;
    }
    catch (InvalidOperationException)
    {
        return GameResult.InvalidOperation;
    }
}

// 使用示例
var result = await SafeExecuteTransaction(userId, builder =>
    builder.AddCurrency("gold", 100)
           .SetData("level", newLevel)
);

性能最佳实践 ​

1. 批量查询优化 ​

csharp
// 推荐:一次查询多个用户
var usersData = await QueryUserDataAsync(
    userIds: allUserIds,
    keys: ["level", "experience"]
);

// 避免:循环单个查询
foreach (var userId in allUserIds)  // 注意:这里是UserId
{
    var userData = await QueryUserDataAsync([userId], keys);
}

2. 事务合并 ​

csharp
// 推荐:一个事务完成所有相关操作
var result = await ForUser(userId)  // 使用UserId
    .CostCurrency("energy", 10)      // 消耗
    .AddCurrency("experience", 100)  // 奖励
    .SetData("last_action", DateTime.UtcNow.ToString())
    .WithDescription("战斗结算")
    .ExecuteAsync();

// 避免:拆分成多个事务
await ForUser(userId).CostCurrency("energy", 10).ExecuteAsync();
await ForUser(userId).AddCurrency("experience", 100).ExecuteAsync();

3. 查询范围限制 ​

csharp
// 大列表使用maxCount限制
var recentItems = await QueryUserListItemsAsync(
    userId: userId,
    key: "inventory",
    maxCount: 50  // 只获取最新50个
);

// 小列表可以不限制
var friends = await QueryUserListItemsAsync(
    userId: userId,
    key: "friends"  // 好友列表通常较小
);

如果需要把大列表分次读完,使用 ListItem 游标扫描,而不是把 maxCount 调到很大:

csharp
long? cursor = null;

do
{
    var batch = await ScanUserListItemsAsync(
        userId: userId,
        key: "inventory",
        batchSize: 100,
        beforeItemUuid: cursor
    );

    if (!batch)
    {
        Game.Logger.LogWarning("扫描列表项失败: {Result}", batch.Result);
        break;
    }

    foreach (var item in batch.Data.Items)
    {
        ProcessInventoryItem(item);
    }

    cursor = batch.Data.NextBeforeItemUuid;
}
while (cursor is not null);

ScanUserListItemsAsync 按 ItemUUID 倒序向更旧记录推进,正常情况下不会重复返回已经扫描过的列表项。它不是强一致快照;扫描过程中新增的记录通常留到下一轮完整扫描,删除或移动列表项会影响后续批次。

调试和故障排除 ​

1. 启用详细日志 ​

csharp
var result = await ForUser(userId)
    .AddCurrency("gold", amount)
    .WithDescription($"操作详情 - 玩家:{userId}, 数量:{amount}, 时间:{DateTime.UtcNow}")
    .ExecuteAsync();

2. 事务内容检查 ​

csharp
var builder = ForUser(userId)
    .SetData("level", 10)
    .AddCurrency("gold", 100);

// 调试:检查将要执行的操作
var operations = builder.Build();
foreach (var op in operations)
{
    Game.Logger.LogDebug("操作: {Type}, 键: {Key}, 值: {Value}", op.Type, op.Key, op.Value);
}

var result = await ExecuteTransactionAsync(operations, "调试事务");

3. 超时处理 ​

csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var result = await ForUser(userId)
        .SetData("data", value)
        .ExecuteAsync()
        .WaitAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Game.Logger.LogWarning("操作超时,请检查网络连接");
}

常见错误和解决方案 ​

错误原因解决方案
ArgumentException: UserId must be positive用户ID无效确保用户ID > 0
ArgumentException: Key cannot be empty键名为空检查键名是否有效
UserCloudDataResult.InsufficientCurrency货币不足先查询当前货币再执行扣除
UserCloudDataResult.CapExceeded上限数据超出设定上限检查ModifyCappedData操作的增量值
UserCloudDataResult.TransactionCommitEmpty空事务确保事务包含有效操作
UserCloudDataResult.ServiceNotInitialized云数据服务尚未初始化启动阶段先 await CloudData.WaitUntilReadyAsync(),不要用短间隔循环撞时机
UserCloudDataResult.FailedToSend网络问题检查网络连接,考虑重试
UserCloudDataResult.TooManyReadOperations查询过于频繁合并查询并降低请求频率
UserCloudDataResult.TooManyWriteOperations提交过于频繁合并事务,稍后重试
UserCloudDataResult.MessageSizeTooLarge请求或响应数据过大缩小或拆分数据;大 ListItem 列表用游标分批扫描

下一步 ​