跳到主要内容

云变量API文档

通用存储、列表、货币、名字:这些功能统称为云变量(在部分老文档里,也把这部分功能统称为积分)

所有云变量接口都运行在协程内, 所以调用者必须确保自己处于协程中, 每个和远程服务(RPC)交互的操作调用后都会等待直到处理结果返回.

几乎所有接口的参数都有且只有一个table(少数接口没有任何参数)

返回结果通常有三个值, 分别是error_code, data, err_msg

  • error_code: 为0时, 代表操作成功. 否则将被视为有某种错误
  • data: 当error_code == 0时, data是本次操作的结果. 否则, data可能是服务器方面的某种错误原因, 也可能为nil. 所以使用data前, 一定要先判断error_code是否为0
  • err_msg: 当error_code == 0时, err_msg为nil, 否则err_msg为一个字符串, 描述错误信息
-- coroutine.async可以创建一个协程. 以下假设所有调用都处于协程之内, 不再使用coroutine.async包裹
coroutine.async(function()
local error_code, data, err_msg = score.get({
user_id = '123456',
key = 'bar',
})

if error_code == 0 then
log.info(fmt("数据: %s, json.encode(data"))
else
log.error(fmt("错误码: %s, 错误数据: %s, 错误信息: %s", ec, json.encode(data), err_msg))
end
end)

注意:

  1. 每个游戏局在现实时间每分钟(从第一次提交开始计时,并非严格意义上的分钟)内,最多执行300次读操作和300次写操作。
  2. 云变量单次读写不得超过32M,每分钟写入操作总大小不得超过48M。
  3. 写操作次数=commit的次数+message_send的次数+message_modify_read的次数+message_delete的次数。

通用接口

get_commit (非RPC)

获得一个提交对象。除了统计,所有的修改操作都必须在提交对象上进行。可以把有关联的,必须同时成功的修改操作放在同一个提交对象里。

local c = score.get_commit()

commit

结束之前获得的提交对象,真正提交所有修改。在commit函数返回之前,所有相关修改未必真正的写入了数据库。

-- 示例
local c = score.get_commit()

-- 设置apple的初值为5
c.set {user_id = 55555555, key = 'apple', i_value = 5}

-- 添加2个apple
c.addi {user_id = 55555555, key = 'apple', value = 2},

-- 设置apple的属性
c.set {user_id = 55555555, key = 'apple', value = {color = 'red', price = 100}

-- 设置hp初值为100, 且设置'魔障'属性
c.set {user_id = 55555555, key = 'hp', value = {['魔障'] = 10}, i_value = 100}

-- 增加1000 HP
c.addi {user_id = 55555555, key = 'hp', value = 1000},

-- 扣除'星辰之力' 2点.
-- 注意, 如果value = 2改为 value = 4, 那么本次commit会失败, 因为'星辰之力'不足, 整个committer里的所有操作都无效化
c.money_cost {user_id = 55555555, key = '星辰之力', value = 2}

c.world_list_add({user_id = 55555555, key = '砖', value = {a = 1}, world_id = 777})

c.world_data_set({user_id = 55555555, key = '种子', value = {b = 1}, world_id = 777})

local error_code, data, err_msg = c.commit()
if error_code == 0 then
log.info('写入数据成功')
else
log.error(fmt('写入失败, 错误码: %s, 错误信息: %s, 额外错误信息: %s', error_code, err_msg, json.encode(data)))
end
  • user_id是number类型, 比如55555555, 不过通常你也可以传入字符串形式的"55555555", 但是要保证该字符串可以被合法的转为number
  • response里的user_id总是number
  • key是一个字符串, 代表二级分类. 比如:
{user_id = 45646546, key = '装备', value = {'手榴弹' = 3, '子弹' = 1053, 'AK47' = 1}}
  • 参数
  • 返回的data格式
    data = {}  -- 空table

test_cloud_value

检测该值是否可以存进云变量,并返回序列化后的字节数。如果待检测的值合法,则打印序列化后的长度;否则报错提示非法原因。

-- 示例
local map = __TS__New(
Map,
{
__TS__Keyword("number"),
__TS__TypeReference(Unit, {})
},
__TS__New(Array, {{kind = "TupleType"}})
)
map.set(1, base.player(1):get_hero())

-- error_code返回0表示可以存储,其他值为不可存储
-- length为云变量序列化后的长度
-- err_msg为错误信息
-- 运行这句会报错:参数含有Map或Set等无法上传的对象
local error_code, length, err_msg = score.test_cloud_value { value = map }

通用存储

score.get

查询数据

local error_code, data, err_msg = score.get({
user_id = '55555555', -- 也可以用字符串, 但要保证可以合法转为number, 下同
key = 'apple',
})

如果调用成功, 则返回的data

-- 示例
data = {
{
i_value = 7,
key = "apple",
user_id = 55555555,
value = {
color = "red",
price = 100
}
}
}
  • user_id: number, 但是也可以传入一个number[]类型, 做批量查询
  • key: 字符串, 但是也可以传入一个string[]类型, 做批量查询

user_idkey可以同时都是数组, 实现复合查询

committer.set

设置云变量

local c = score.get_commit()

c.set {
user_id = '55555555',
key = '属性',
value = {['攻击'] = 10, ['护甲'] = 20},
i_value = 100,
s_value = '必胜!',
}

local error_code, data, err_msg = c.commit()
  • 参数
    • user_idkey: 上文已经解释
    • value: 可选, 是一个table, 应遵守云变量的值规则
    • i_value: 可选, 是一个number, 会四舍五入成整数
    • s_value: 可选, 是一个string

可以每次设置value/i_value/s_value中的一个或多个。设置多个值时,会按照i_value、s_value、value的顺序依次执行。比如传入的是

c.set  {
user_id = 55555555,
key = '属性',
i_value = 100,
s_value = '必胜!',
}

则只会设置i_value和s_value, value不会受到任何影响(以前设置过就不变, 没设置过就保持为nil)。

注意: 如果i_value不是数字,会直接报错attempt to add a 'string' with a 'number',整条提交全部回滚。

committer.c.add

累加数字到i_value

c.add {
user_id = '55555555',
key = 'hp',
i_value = 1,
}

这会使i_value加到{user_id = '55555555', key = 'hp'}的行 如果该行不存在, 则视同committer.set操作

金钱存储

特属于通用存储, 金钱存储内只有数字字段, 且消费时不能使存额降为负值(会使commit()操作失败)

score.money_get

查询金钱

local ec, data, err_msg = score.money_get({
user_id = '55555555',
key = '星辰之力',
})

如果调用成功, 则返回的data

data = {
{
key = "星辰之力",
user_id = 55555555,
value = 1
}
}

user_idkey的意义与score.get里的user_idkey相同, 都可以传入数组

committer.money_add

累加金钱

c.money_add({
user_id = '55555555',
key = '星辰之力',
value = 2,
})

c.add类似, 但是value必须为整数, 否则会报错,且整条提交全部回滚。也可以用money_add把值扣为负数,一般是误发了游戏资源后手动扣掉。

出于安全考虑,不能直接用API设置金钱

committer.money_cost

扣除金钱

c.money_cost({
user_id = '55555555',
key = '星辰之力',
value = 2,
})

c.money_add类似, 但是value必须为正值, 否则会报错.

特别注意: 如果扣除后结果为负数, 则会返回失败(用于购买物品等场景),整条提交全部回滚

列表服务

列表相关,不同于统计的是提供增删改

如果有两局同时对同一个key的列表进行了list_add是没事的。会产生不同的list_id。

如果有两局同时对同一个key的列表的不同list_id进行了list_modify操作,那也没事。但是对同一个list_id进行list_modify,就会有覆盖的问题

list_add

列表新增项

local uuid1 = c.list_add({
user_id = '55555555',
key = '背包格子',
value = {
name = 'AK47',
level = 1,
item_id = 1234567,
}
})

local uuid2 = c.list_add({
user_id = '55555555',
key = '背包格子',
value = {
name = 'M16',
level = 2,
item_id = 1234568,
}
})

-- 以上会同时添加AK47和M16到列表存储中

list_add操作会立即返回一个uuid

这个uuid可以让你在还没有commit时就可以自由存储于其他地方(但是如果后来commit失败, 这些uuid也就失效了)

list_modify

列表修改项

c.list_modify({
uuid = 31231453543, -- 一个64bit的数字
value = {
name = 'M16',
level = 3, -- 给M16升级
item_id = 1234568,
}
})

list_delete

列表删除项

c.list_delete({
uuid = 3123242341, -- 一个64bit的数字
})

list_query

列表查询,返回的数据按创建最后一次修改时间倒序排序

  • 参数
    • user_idkey: 上文已经解释,必填
    • timetype: 可选,是一个字符串,如果这里传一个字符串stamp,则返回的updateAt是一个以秒为单位的时间戳,类似1587524484这样。否则(默认情况),返回的是一个时间,类似2020-04-22 11:01:24
    • limit: 可选,是一个number,返回指定数量的数据,不填则返回所有数据(没有条数限制)
local error_code, data, err_msg = score.list_query({
user_id = 55555555,
key = '背包格子'
})
-- 如果调用成功, 则返回的`data`为
data = {
{
uuid = 123456788,
value = {
item_id = 1234568,
name = "M16",
level = 2
},
key = "背包格子",
updatedAt = "2024-07-02 18:23:34", -- 这个字段是创建时间,非更新时间
user_id = 55555555
},
{
uuid = 123456789,
value = {
item_id = 1234567,
name = "AK47",
level = 1
},
key = "背包格子",
updatedAt = "2024-07-02 18:23:34",
user_id = 55555555
},
}

list_query_by_uuid

根据uuid进行列表查询

  • 参数
    • uuid: 必选,一个64bit的数字,是列表型云变量上传或修改返回的uuid
local error_code, data, err_msg = score.list_query_by_uuid({
uuid = 123456789,
})
-- 如果调用成功, 则返回的`data`为
data = {
{
user_id = 55555555,
uuid = 123456789,
key = "背包格子",
updatedAt = "2024-07-02 18:23:34",
value = {
name = "AK47",
level = 1,
item_id = 1234567
}
},
}

消息相关

收发消息全部用message相关,包括离线消息、在线消息

score.message_send

发消息。发送的消息除了可以用message_query查到,更常用的还是用subscribe_channel来订阅。消息离线也可以收到。

local error_code, data, err_msg = score.message_send({
src_user_id = 55555555,
key = '聊天_1',
target_user_id = 66666666,
read = 0,
deleted = 0,
value = {
信息 = '天气不错',
附加信息 = '间谍卫星探测到, 闪电风暴即将来临'
}
})
  • src_user_id: 消息发出者
  • key: 二级类别分割
  • target_user_id: 消息接收者
  • read: 初始化是否已读, 如果不填, 默认是0(未读)
  • deleted: 初始化是否已删除, 如果不填, 默认是0(未删除)
  • value: 一个table, 与commiter.set里的value相仿

score.message_query

查询player相应key的消息,只会返回未读消息(以后会加查询已读、未读和已读的参数)

local error_code, data, err_msg = score.message_query({
src_user_id = 55555555,
key = '聊天_1',
target_user_id = 66666666,
read = 0,
})

查询55555555在"聊天_1"里给66666666发的所有未读消息

  • read: 1: 代表已读, 2: 代表未读, 不填(nil): 代表未读和已读都查

如果调用成功, 则返回的data

data = {
{
deleted = 0,
key = "聊天_1",
objectId = 596437695679, -- 一个64bit的数字
read = 0,
src_user_id = 55555555,
target_user_id = 6666666,
createAt = "2022-12-09 15:34:19",
value = "asdasdqwe"
}
}

score.message_modify_read

设置是否已读

local error_code, data, err_msg = score.message_modify_read({
objectId = 596437695679, -- 上面message_query查出的objectId字段
new_read_value = 1 -- 代表已读
})

score.message_delete

设置是否已删除

local error_code, data, err_msg = score.message_delete({
objectId = 596437695679 -- 上面message_query查出的objectId字段
})

名字服务

名字

  • 支持每key范围唯一起名
    • 不存在才允许插入键值对,否则失败
  • 支持以索引(key)的子串搜索
    • 暂不支持拼音首字母的搜索

score.name_new

c.name_new({
key = '游戏角色名',
name = '大水哥',
value = '55555555' -- 让大水哥这个名字属于55555555
})
  • key: 分割类型
  • name: key下的唯一名, 如果key+name已经存在, 则本次commit失败, 所有操作失效
  • value: 字符串, 可以随便存些数据, 示例里存了归属于的user_id

搜索开头包含指定字符串的所有名字(前缀匹配)

-- 示例
local c = score.get_commit()

c.name_new({
key = '游戏角色名',
name = '大水哥',
value = '55555555' -- 让大水哥这个名字属于55555555
})

c.name_new({
key = '游戏角色名',
name = '天神大水哥',
value = '55555555' -- 让大水哥这个名字属于55555555
})

c.name_new({
key = '游戏角色名',
name = '大水哥666',
value = '55555555' -- 让大水哥这个名字属于55555555
})

local ec = c.commit()
if ec ~= 0 then
error ('name_new commit failed')
end

local error_code, data, err_msg = score.name_search({
key = '游戏角色名',
name_prefix = '大水哥',
})
-- 如果调用成功, 则返回的`data`为
data = {
{
key = "游戏角色名",
name = "大水哥",
value = "55555555"
},
{
key = "游戏角色名",
name = "大水哥666",
value = "55555555"
}
}

-- 可以看到, "天神大水哥"没有搜索到

底层消息

score.subscribe_channel

消息订阅。订阅发给player的key类型的消息。player可以是一个虚拟的用户(你确定不会真实存在的uid,比如1 2 3)。即使这个消息是你订阅前发的,你也能收到。收到消息后通常需要把消息标记为已读,否则你下次调用subscribe_channel或message_query时,还将会再次收到该消息。

同一局内,重复订阅特定用户的同一个频道的话,第二次的订阅是无效的。

由c++实现的api

  • 参数
    • player (player/integer/nil) - 玩家
    • key (string) - 消息的key
  • 返回
    • ret_value (boolean) - 表示是否成功订阅。目前失败只发生在重复的订阅
  • 回调返回
    • src_user_id (integer) - 消息源用户id
    • target_user_id (integer) - 消息目标用户id
    • key (string) - 消息的key
    • read (boolean) - 标记已读未读
    • value (string/integer/table/nil) - 消息的值
    • message_id (integer) - 消息的唯一标识
score.subscribe_channel(player, 'key', {
ok = function (result)
print(result.src_user_id)
print(result.target_user_id)
print(result.key)
print(result.read)
print(result.value)
print(result.message_id)
end,
})

score.unsubscribe_channel

取消订阅。

这个方法通常不用被显示调用,因为用户下线时会自动取消订阅该用户相关的消息订阅;当前游戏局结束时会自动取消所有本局产生的订阅(包括关于虚拟用户的订阅)

由c++实现的api

  • 参数
    • player (player/integer/nil) - 玩家
    • key (string) - 消息的key
  • 返回
    • src_user_id (integer) - 消息源用户id
    • target_user_id (integer) - 消息目标用户id
    • key (string) - 消息的key
    • read (boolean) - 标记已读未读
    • value (string/integer/table/nil) - 统计的值
    • message_id (integer) - 消息的唯一标识
score.unsubscribe_channel(player, 'key')

score.subscribe_message

更底层的消息订阅,且只支持在线消息。即如果A先往channel1发了个消息,B才订阅channel1,B是收不到消息的。

因为是纯在线消息,因此只能通过回调收到消息,没有单独的查询方法。

和publish_message组合使用。

同一局内,重复订阅同一个频道的话,第二次的订阅是无效的。

由c++实现的api

注意,另一个接口subscribe_channel相当于会订阅userid .. '_' .. key的channel(下划线连接),注意不要使用相同的字符串拼装方式,以免重复

目前ac层一些需要和外部接口交互的机制也用了subscribe_message和publish_message,如支付和实名认证。

  • 参数
    • channel_name (string) - 订阅或者监听的消息的频道
  • 返回
    • ret_value (boolean) - 表示是否成功订阅。目前失败只发生在重复的订阅
  • 回调返回
    • 会返回一个table,table里只有一个字段,叫message (table)
score.subscribe_message('channelxxx', {
ok = function (result)
print(result.message)
end,
})

publish_message

发布消息,与subscribe_message组合使用,不走离线消息只走在线消息

由c++实现的api

  • 参数
    • channel_name (string) - 频道名,需要发送的的目的地
    • value (table) - 消息体
score.publish_message('channelxxx', { test = 1})

世界积分

score.world_data_get

查询世界数据

local error_code, data, err_msg = score.world_data_get({
user_id = 100,
key = 'test_world_data',
world_id = 777
})

如果调用成功, 则返回的data

data = {
{
key = "test_world_data",
user_id = 100,
value = {
color = "123",
price = 100
}
}
}

修改世界数据

local c = score.get_commit()

c.world_data_set({
user_id = 55555555,
key = '种子',
value = {b = 1},
world_id = 777
})

local error_code, data, err_msg = c.commit()

参数 *

  • user_idkey: 上文已经解释
  • value: 可选, 是一个table, 应遵守云变量的值规则

世界列表服务

world_list_add

列表新增项

local uuid1 = c.world_list_add({
user_id = '55555555',
key = '背包格子',
value = {
name = 'AK47',
level = 1,
item_id = 1234567,
},
world_id = 777
})

-- 以上会添加AK47到列表存储中

list_add操作会立即返回一个uuid

这个uuid可以让你在还没有commit时就可以自由存储于其他地方(但是如果后来commit失败, 这些uuid也就失效了)

world_list_modify

列表修改项

c.world_list_modify({
uuid = 31231453543, -- 一个64bit的数字
value = {
name = 'M16',
level = 3, -- 给M16升级
item_id = 1234568,
}
})

world_list_delete

列表删除项

c.world_list_delete({
uuid = 3123242341, -- 一个64bit的数字
})

world_list_query

列表查询. 返回的数据按创建最后一次修改时间倒序排序

local error_code, data, err_msg = score.world_list_query({
user_id = 55555555,
key = '背包格子',
world_id = 777
})
-- 如果调用成功, 则返回的`data`为
data = {
{
key = "背包格子",
user_id = 55555555,
uuid = 1234567890123, -- 一个64bit的数字
value = {
item_id = 1234568,
level = 2,
name = "M16"
},
world_id = 777,
},
{
key = "背包格子",
updatedAt = "2022-12-09T14:00:23.539Z",
user_id = 55555555,
uuid = 1234567890124, -- 一个64bit的数字
value = {
item_id = 1234567,
level = 1,
name = "AK47"
},
world_id = 777
},
}

注意, 每次查询仅能最多查出1000行数据

world_list_query_by_uuid

根据uuid进行列表查询

local error_code, data, err_msg = score.world_list_query_by_uuid({
uuid = 1234567890123,
})
-- 如果调用成功, 则返回的`data`为
data = {
{
key = "背包格子",
user_id = 55555555,
uuid = 1234567890123, -- 一个64bit的数字
value = {
item_id = 1234568,
level = 2,
name = "M16"
},
world_id = 777,
},
}

带限制的云变量

1. 添加与修改

举个例子:

local c = score.get_commit()

c.withlimit_add { user_id = 55555555, key = '限定任务', value = 1, limit = 3 }

local err_code, data, err_msg = c.commit()

这表示给「限定任务」这个key对应的value+1,但不得超过上限3,超过则事务失败。value允许传负数(用于给例如「肝任务卡」之类的清任务的道具用),但也不允许扣到负数,扣成负数则整个提交全部失效

对于需要每隔一段时间刷新的任务,我们提供了refresh_xxx参数,服务器会记录修改时间。

1.1 refresh_hour

具体来说,是4个「自然小时」(这是为了和下面的自然周、自然月保持统一),即我如果1:59 add过,5:00就可以再次add了(实际才过3个小时出头),也就是说只看整小时的部分。

c.withlimit_add { user_id = 55555555, key = '四小时内只能做一次的任务', value = 1, limit = 1, refresh_hour = 4 }

1.2 refresh_day

每n天刷新的带限制云变量,注意:刷新时间是n天后的0点。

c.withlimit_add { user_id = 55555555, key = '每日任务', value = 1, limit = 3, refresh_day = 1}

1.3 refresh_week

考虑周的开始可能有些人觉得是周日,有些人觉得周一,额外加个week_start参数,必须是{'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'}其中之一,可以为空,为空时默认为'monday'。

c.withlimit_add { user_id = 55555555, key = '每周任务', value = 1, limit = 3, refresh_week = 1, week_start =  'monday'}

如果我不想用自然周,我就是想这次任务做了,再过7天才能做呢?那你可以用refresh_day = 7,或者refresh_hour = 7 * 24。

1.4 refresh_month

1.5 refresh_year


注意!如果对同一个user_id的同一个key,两次调用withlimit_add时,refresh参数不同(类型或数值),前一次的结果会被直接覆盖掉。

c.withlimit_add { user_id = 55555555, key = '每日任务', value = 1, limit = 1000, refresh_day = 1 }
c.withlimit_add { user_id = 55555555, key = '每日任务', value = 10, limit = 1000, refresh_day = 3 }
c.withlimit_add { user_id = 55555555, key = '每日任务', value = 100, limit = 1000, refresh_hour = 5 }

-- 最终结果是: {
-- user_id = '55555555',
-- key = '每日任务',
-- value = 100,
-- limit = 1000,
-- refresh_hour = 5,
-- }

但是,对同一个user_id的同一个key,两次调用withlimit_add时,refresh_xxx参数相同,而limit参数不同,这是合法的。

比如,某个每日任务完成次数上限为3,购买特权后上限额外+1。

c.withlimit_add { user_id = 55555555, key = '每日任务', value = 3, limit = 3, refresh_day = 1 }
c.withlimit_add { user_id = 55555555, key = '每日任务', value = 1, limit = 4, refresh_day = 1 }

-- 最终结果是: {
-- user_id = '55555555',
-- key = '每日任务',
-- value = 4,
-- limit = 4,
-- refresh_day = 1,
-- }

2. 查询

local err_code, data, err_msg = score.withlimit_query { user_id = 55555555, key = '每年限定任务' }
-- 如果调用成功, 则返回值为
-- err_code = 0
-- data = {
-- key = "限定任务",
-- user_id = 55555555,
-- value = 1, -- 一个64bit的数字
-- limit = 3, -- 一个64bit的数字
-- refresh_week = 1,
-- week_start = 'monday',
-- }

注意这个接口是可能修改数据的!当过了刷新时间,会改value为0,并且返回0。

错误码

云积分操作,最好把返回的错误码打印log,方便排查问题

1213
含义:死锁错误,两次云积分的commit同时触发,并且操作的数据的顺序不一致

1205
含义:锁等待超时错误

7534
含义:突发流量,导致并发量急剧上升,数据库底层自动限流

建议:
1、确保云积分的commit里面的操作是按一致的顺序提交的
2、确保没有重复多次提交commit相同数据的情况
3、优化commit里面操作的复杂度,避免耗时过长

1405
数据太长,超出最大长度限制

建议:

  1. 如果预期某个key的score会很大,可以设计拆成多个key来存储
  2. 不要存无用数据在score里面,尽量压缩存储的信息

查询服务

score.is_old_player

查询玩家是否是当前游戏作者的其他游戏的老玩家(以开过局为准)

local error, data, error_msg = score.is_old_player({user_id = 1700953034})

如果调用成功, 则返回的data

data = true(or false)