敏感词导出和上报
1. 使用场景
游戏业务数据有敏感词扫描的需求,数据一般是名称类的字段(如昵称、公告)以及其关联的其他信息(如用户ID、区ID等), 定期地(每日)导出到文件,然后上报给安全侧进行处理。
朴素的方法是,使用TcaplusDB的分析型文本导出功能, 或者使用tcaplus_client工具的dump命令, 导出指定表的原始数据,然后进行业务定制的数据清理,再上报给安全侧。
有不少问题和限制:
- 导出的数据比较原始,如导出数据是一级字段,但目标数据是嵌套很多层,每个业务都实现对应的清理逻辑才能得到目标数据
- 导出的原始数据格式较复杂,清理逻辑繁琐,比如TDR结构中嵌套复杂的Proto格式的数据
- 业务自己都需要维护一套导出脚本,有运维成本
- tcaplus_client工具的dump命令,是遍历在线数据,可能对在线读写带来性能负担
有很多游戏业务都有这种使用场景,导出的数据也有很多共性,因此我们提供一种灵活的导出数据和直接上报到安全侧的功能。
大概地,通过写一条类SQL的语句,即可配置导出定制格式的数据,如:
-- 支持SELECT任意嵌套的字段,支持条件过滤
SELECT
model_data['basic'].basic_info.declaration AS declaration,
model_data['basic'].basic_info.guild_name AS guild_name,
INT(0) AS vOpenID,
INT(0) AS platid,
model_data['basic'].basic_info.zone_id AS zone_id,
model_data['basic'].basic_info.guild_id AS guild_id,
INT(0) AS big_zone_id
FROM GuildActorData
INTO 'xxx-guildname-0-20250527.txt'
WHERE actor_id LIKE 'guild:%' AND actor_id NOT LIKE 'guild:%:%';
2. 术语
源表,即TcaplusDB中的表,使用app_id
(业务ID)、zone_id
(区ID)和table_name
唯一标识。一般,一个业务的不同的区(如QQ或微信区)下都有相同的表。
导出视图,类似传统SQL中视图
的概念,即一条对源表查询的SELECT
SQL语句,如前面示例的SQL。
目标表,从源表导出的目标数据的命名,如前面示例的'xxx-guildname-0-20250527.txt'中的guildname
,一个源表可以导出多个目标表(对应多个视图)。
CCID,安全侧为每个业务提供一个CCID,上报数据用,如前面示例的'xxx-guildname-0-20250527.txt'中的xxx
。
3. 配置方法
目前TcaplusDB没有提供对外的OMS页面或者接口,让业务可自助配置导出和上报,可联系Tcaplus_Helper(人工服务)
协助配置。
请提供以下导出配置信息:
- CCID,安全侧提供。
- 源表,即
app_id
、table_name
和一个或多个zone_id
。若有多个zone_id
,那么就是多个区的相同源表作为数据源进行导出,合并。 - 导出视图,导出SQL和目标表的命名,一个源表可以配多个导出视图。SQL支持哪些能力和语法详见后文。
- Proto定义(可选),如果PB数据是以Bytes类型存储在TcaplusDB中,没有Proto定义后台DB是无法解析数据。但也无需提供原始完整的Proto定义,只需提供必要的部分定义(足够解析数据)即可。
- 别名前缀(可选),一个业务可能需要多套上报(如正式服和体验服分开),但只有一个CCID,需要使用一个别名前缀区分,如体验服上报为'xxx-tiyan_guildname-0-20250527.txt'。
其他说明:
- TcaplusDB有每日备份数据,导出的是当天凌晨备份的静态数据,非线上实时数据。
- 导出任务每日凌晨触发导出并上报,少数情况下若有异常(如当天备份失败),当天白天会人工恢复,或者第二天会继续上报。
- 当源表是多区或多分片时,对应上报的也是多文件,有编号做后缀,如
xxx-guildname-0-20250527.txt.1
、xxx-guildname-0-20250527.txt.2
。
4. 配置示例
4.1 示例1,TDR数据中嵌套PB数据
TDR表定义,其中嵌套的additional是PB数据:
<?xml version="1.0" encoding="GBK" standalone="yes" ?>
<metalib tagsetversion="1" name="tcaplus_tb" version="20" >
<struct name="PlayerInfo" version="1" primarykey="uin" splittablekey="uin" >
<entry name="uin" type="uint32" desc="游戏ID" notnull="true" />
<entry name="openid" type="string" size="130" desc="openid" />
<entry name="name" type="string" size="64" desc="角色名" />
<entry name="platid" type="uint32" version="19" desc="平台ID" />
<entry name="additional_len" type="int32" desc="protobuf.len" />
<entry name="additional" type="char" count="65536" desc="protobuf protobuf二进制 具体定义在 common_data.proto:PlayerAdditionalData" refer="additional_len" />
<entry name="section_data_5_len" type="int32" desc="protobuf.len" />
<entry name="section_data_5" type="char" count="65536" desc="protobuf protobuf二进制 具体定义在 common_data.proto:DBPlayerPetInfo" refer="section_data_5_len" />
</struct>
</metalib>
其中additional的proto定义如下
message PlayerCardBriefInfo {
// ...
bytes card_signature = 5; // 签名
}
message PlayerAdditionalData {
uint32 player_image_id = 1; // 玩家形象ID
// ...
PlayerCardBriefInfo card_brief_info = 5; // 玩家个人名片概要信息
}
CCID=123,目标表名是nitice
,要求导出格式为(card_signature, openid, platid, 0, 9999, 9999)
,则导出SQL如下
SELECT
additional.card_brief_info.card_signature AS card_signature,
STRING(openid) AS openid,
UINT32(platid) AS platid,
INT(0) AS areaid,
INT(9999) AS f5,
INT(9999) AS f6
FROM PlayerInfo
INTO '123-nitice.txt'
5. SELECT SQL语法
SELECT的字段,支持一下语法能力。
常量
- 如
INT(9999)
,其中INT
函数调用用于提示该常量的类型。
一级字段
- 如
openid
。 - 但是目前暂不支持TDR的多级嵌套字段。
PB的多级嵌套字段
- 普通嵌套,如
additional.card_brief_info.card_signature
。 - map结构的嵌套,如
model_data['basic'].basic_info.declaration
,这里model_data
是map类型,'basic'
是key。 - 数组结构的嵌套(暂未支持,有需求可支持),如
model_array[1].basic_info.declaration
,这里model_array
是repeated类型,[1]
是数组下标。
5.1. Repeated字段展开
有些场景,除了要导出一级字段,还需要导出所有Repeated字段,并且和一级字段联合作为一行导出。
示例,还是PlayerInfo表,其中section_data_5字段的PB定义是DBPlayerPetInfo。
message PetData {
uint32 gid = 1;
bytes name =3;
}
message PlayerPetInfo {
repeated PetData pet_data = 1;
}
message DBPlayerPetInfo {
PlayerPetInfo pet_info = 3;
}
CCID=123,目标表名是spiritname
,即精灵名称,要求导出格式为(name, openid, platid, 0, 9999, 9999, gid)
,则导出SQL如下
SELECT
EXPAND_ARRAY(section_data_5.pet_info.pet_data, name) AS name,
STRING(openid) AS openid,
UINT32(platid) AS platid,
INT(0) AS areaid,
INT(9999) AS f5,
INT(9999) AS f6,
EXPAND_ARRAY(section_data_5.pet_info.pet_data, gid) AS gid
FROM PlayerInfo
INTO '123-spiritname.txt'
注意上述的EXPAND_ARRAY的调用,第一个参数是section_data_5.pet_info.pet_data是repeated字段(嵌套message结构),name和gid分包是repeated字段的嵌套字段。
若一个player,对应地有N个精灵时,展开数组之后,则会导出N行,即repeated嵌套字段和一级字段进行JOIN联合了。
可以这里理解,对应于传统关系数据库中,相当于DBPlayerPetInfo表和PlayerInfo表进行JOIN。
5.2. 其他特殊场景
导出目标字段,可能存在这种情况:
- 源表数据中没法获取,需从别处获取再和源表数据组合起来。
- 需要做一些稍微复杂的计算,才能得到目标字段,超出SQL表达式的表达能力。
针对这种情况,业务可配置一个后置的处理脚本,python或shell。
输入sql导出的文本,如123-spiritname.txt
(|
分隔的csv文件),脚本处理输出最终上报的文本。
示例如下,输入第三列(row[2]
)的字段regist_zone,计算出sys_id、plat_id,再重新拼接最终上报的row。
def post_process(input_line : str):
row = input_line.split('|')
sys_id = 0
plat_id = 9999
regist_zone = int(row[2])
if regist_zone > 100000 and regist_zone <= 200000:
sys_id = 0
plat_id = 1
elif regist_zone > 200000 and regist_zone <= 300000:
sys_id = 1
plat_id = 1
elif regist_zone > 300000 and regist_zone <= 400000:
sys_id = 0
plat_id = 2
elif regist_zone > 400000 and regist_zone <= 500000:
sys_id = 1
plat_id = 2
ouput_row = row[0:2] + [str(sys_id), str(plat_id)] + row[2:]
return '|'.join(ouput_row)
6. 条件过滤语法
如上示例中的... WHERE actor_id LIKE 'guild:%' AND actor_id NOT LIKE 'guild:%:%'
,支持条件过滤,仅导出满足条件的记录。
目前仅支持简单的几种过滤表达式:
- 字符串模糊匹配,即LIKE通配,语法同mysql。
- 整型的大小比较,如
UINT32(guild_id) > 10000
。 - AND、OR、NOT逻辑运算。
理论上能支持更多过滤语法,更多的过滤也可以联系Tcaplus_Helper(人工服务)
补充实现,后续可支持的条件过滤见条件过滤语法说明。