敏感词导出和上报

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_idtable_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.1xxx-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(人工服务)补充实现,后续可支持的条件过滤见条件过滤语法说明

results matching ""

    No results matching ""