MeteorCat / 强关联架构

Created Thu, 16 Jan 2025 21:14:39 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
3105 Words

强关联架构

之前说过强关联数据的复杂情况( 涵盖有交叉切库和多表关联的关系 ), 当时想着早晚会遇到这种需求, 没想到这么快就遇到了.

这里涉及到的有以下关联:

  • 游戏服务器组(ServerGroup): 将大量服务器分组, 每个组关联多个服务器放置在同个数据库, 该数据库对应组ID
  • 游戏服务器(Server): 游戏服务器, 放置设计游戏关联信息的
  • 渠道来源(Channel): 来源渠道, 比如快手|抖音|微信或者商店对应来源渠道
  • 后台管理员(User): 后台管理员, 不同管理员关联不同渠道|不同服务器|不同权限等
  • 后台权限(Auth): 访问权限, 管理员的权限各有不同

这里强烈这种需求尽可能 不要用脚本语言去实现, 特别是弱类型语言维护坑太多了; 复杂关联数据本身就很绕了, 还要处理数据类型传递验证且没有常驻数据连接池访问的问题.

弱类型后续维护坑太大了, 主要是弱类型写着最后传递过来的类型分不清是 int|string|null 哪种, 不注意的时候传递类型直接被修改都不清楚情况.

这里关联数据的方式先说明, 这里授权采用传统 RBAC 建模, 单个用户关联多个角色, 角色有:

  1. 用户登录后台(User 检索数据)
  2. 判断用户是最高管理员或者普通管理员( 只有最高管理员默认拥有所有权限 )
  3. 获取关联角色|分组( Group|Role 检索关联 )
  4. 通过角色|分组关联获取权限( Auth 检索菜单和接口权限 )
  5. 通过角色|分组关联获取渠道( Channel 检索关联渠道 )
  6. 通过角色|分组关联获取服务器( Server 检索关联服务器 )

后续所有数据都要去 ServerId IN (1,2,3) AND Channel(1,2,3,4) 这里追加条件, 单单要建立这里面关联就需要以下表:

# 管理员表, 最基础简单的账户表
CREATE TABLE IF NOT EXISTS `user`
(
    `id`          bigint(20) unsigned                    NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `username`    varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号名',
    `nickname`    varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '昵称',
    `salt`        varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '盐值',
    `password`    varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '哈希密码',
    `token`       varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权Token',
    `created_at`  int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '创建时间',
    `created_uid` int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '创建管理员',
    `created_ip`  varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建IP',
    `updated_at`  int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '更新时间',
    `updated_uid` int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '更新管理员',
    `updated_ip`  varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '更新IP',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='管理员账户表';


# 管理员对应分组, 给后台划分角色类型, 比如策划|主管|客服等角色
CREATE TABLE IF NOT EXISTS `user_group`
(
    `id`   bigint(20) unsigned                    NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色组名称',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='管理员角色组';


# 用户权限表
CREATE TABLE IF NOT EXISTS `user_auth`
(
    `id`   bigint(20) unsigned                     NOT NULL AUTO_INCREMENT COMMENT '主键',
    `pid`  bigint(20) unsigned                     NOT NULL DEFAULT '0' COMMENT '等于0标识父级节点, 大于0是关联的父节点',
    `ty`   int(10) unsigned                        NOT NULL DEFAULT '0' COMMENT '菜单类型, 0 默认为菜单页面, 1 为API隐藏接口类型',
    `name` varchar(32) COLLATE utf8mb4_unicode_ci  NOT NULL COMMENT '权限名',
    `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限路径',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='管理员权限表';

#  对应渠道来源, 比如快手|抖音|微信|H5等
CREATE TABLE IF NOT EXISTS `channel`
(
    `id`         bigint(20) unsigned                    NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `channel_id` bigint(20) unsigned                    NOT NULL COMMENT '第三方渠道定义',
    `name`       varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '渠道名称',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='渠道分类';


# 服务器列表
CREATE TABLE IF NOT EXISTS `server`
(
    `id`          bigint(20) unsigned                    NOT NULL COMMENT '服务器ID, 需要运维定义',
    `name`        varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务器名称',
    `created_at`  int(10) unsigned                       NOT NULL COMMENT '创建时间',
    `created_uid` bigint(20) unsigned                    NOT NULL COMMENT '创建管理员',
    `created_ip`  varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建IP地址',
    `updated_at`  int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '更新时间',
    `updated_uid` bigint(20) unsigned                    NOT NULL DEFAULT '0' COMMENT '更新管理员',
    `updated_ip`  varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '更新IP地址',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='服务器列表';

这就是基础的信息表, 需要留意基础表数据千万 不要硬删除, 否则会出现关联数据关联错误, 而是采用字段软删除才能保证数据关联; 之后关联的表数据用于 role|group 这些分组ID来检索关系:

# 用户-分组|角色表
CREATE TABLE IF NOT EXISTS `user_group_link`
(
    `id`       bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `group_id` bigint(20) unsigned NOT NULL COMMENT '分组ID',
    `uid`      bigint(20) unsigned NOT NULL COMMENT '管理员ID',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='用户-分组|角色关联';


# 用户-权限关联表
CREATE TABLE IF NOT EXISTS `user_auth_link`
(
    `id`       bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `group_id` bigint(20) unsigned NOT NULL COMMENT '用户分组ID',
    `auth_id`  bigint(20) unsigned NOT NULL COMMENT '权限主键ID',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='用户-权限关联权限';


# 用户-渠道关联表
CREATE TABLE IF NOT EXISTS `user_channel_link`
(
    `id`         bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `group_id`   bigint(20) unsigned NOT NULL COMMENT '分组ID',
    `channel_id` bigint(20) unsigned NOT NULL COMMENT '渠道ID',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='用户-渠道关联渠道';

# 用户-服务器关联表
CREATE TABLE IF NOT EXISTS `user_server_link`
(
    `id`        bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `group_id`  bigint(20) unsigned NOT NULL COMMENT '分组ID',
    `server_id` bigint(20) unsigned NOT NULL COMMENT '服务器主键ID',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='用户-服务器关联对应服务器';

所以在登录的时候, 需要按照以下流程处理:

  1. 检索用户: SELECT id FROM user WHERE username = 'meteorcat' LIMIT 1
  2. 获取分组|角色: SELECT group_id FROM user_group_link WHERE uid = xxx
  3. 获取权限: SELECT autu_id FROM user_auth_link WHERE group_id IN(xxx,yyy)
  4. 获取渠道: SELECT channel_id FROM user_channel_link WHERE group_id IN(xxx,yyy)
  5. 获取服务器: SELECT server_id FROM user_server_link WHERE group_id IN(xxx,yyy)

登录之后所有数据保存在 Redis 后续检索缓存关联, 之后每次检索数据都要通过 server_id IN(xxx) AND channel_id IN(yyy) 拉取; 同时这里还需要直接一次性取出 名称 等数据, 方便放置缓存直接获取而不走数据库再次读取.

这就要求数据对于字段带有统一性, 关键字段必须不要出现诸如 Channel|channelId|channel_id 这种多风格命名的情况.

渲染层与数据中台

这里采用 渲染层 - 中台 方式处理, 页面处理采用 PHP 并转发接口数据到 数据接口.

之所以采用这种架构是因为本身现有老的 PHP 项目需要做兼容, 且不需要额外前端来编写网页界面情况直接套用老模板.

引用 Node 前端方案需要规划 UI 架构且还要进行上线打包情况, 这样虽然更加规范但是也更加繁琐耗时

同时渲染层还顺便负责做数据清理|游戏服务端交互, 比如直接渲染层的PHP可以直接获取授权直接发 Socket 推送游戏服务端给中台架构减低请求压力.

如果采用前端 Vue|React 方案没办法做到这种情况: 拿到授权之后在自己负责推送数据给游戏服务器, 只能采用 HTTP 推送JSON

主要关于数据中台的编程语言选项, 之前同事推荐过用 golang 来做数据中台, 通过暴露 API 承接内部数据的查询和统计筛选; 但是在使用过后台就感觉 golang 可以作为计算密集主要开发语言, 但是作为 Web 服务内部功能库贫乏, 像序列化JSON:

// golang 处理
type LoginReq struct{
    Username String `v:"required" dc:"账号"`
    Password String `v:"required" dc:"密码"`
}

这里对比 Java 采用注解+拦截就能处理更高级的操作, 直接过滤数据一条龙拦截下暴露的错误就行了:

public class LoginForm {


    @NotBlank(message = "账户不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "账号名称只允许字母数字")
    @Size(message = "账户必须在{min}-{max}字符当中", min = 5, max = 64)
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(message = "密码必须在{min}-{max}字符当中", min = 5, max = 64)
    private String password;

}

但是 Java 虽然开发严谨但是太过死板, 大量的 Get|Set 尚且可以通过 lombok 这种方案处理; 大量 dto|vo|pojo|bo 定义太过繁琐了, 单段请求可能需要数据实体(entity|repository), 请求需要 vo 结构验证并传输, 然后通过数据实体查询 entity 包装返回 bo, 最后返回定义 responsedto 等.