强关联架构
之前说过强关联数据的复杂情况( 涵盖有交叉切库和多表关联的关系 ), 当时想着早晚会遇到这种需求, 没想到这么快就遇到了.
这里涉及到的有以下关联:
- 游戏服务器组(
ServerGroup): 将大量服务器分组, 每个组关联多个服务器放置在同个数据库, 该数据库对应组ID - 游戏服务器(
Server): 游戏服务器, 放置设计游戏关联信息的 - 渠道来源(
Channel): 来源渠道, 比如快手|抖音|微信或者商店对应来源渠道 - 后台管理员(
User): 后台管理员, 不同管理员关联不同渠道|不同服务器|不同权限等 - 后台权限(
Auth): 访问权限, 管理员的权限各有不同
这里强烈这种需求尽可能 不要用脚本语言去实现, 特别是弱类型语言维护坑太多了;
复杂关联数据本身就很绕了, 还要处理数据类型传递验证且没有常驻数据连接池访问的问题.
弱类型后续维护坑太大了, 主要是弱类型写着最后传递过来的类型分不清是 int|string|null 哪种, 不注意的时候传递类型直接被修改都不清楚情况.
这里关联数据的方式先说明, 这里授权采用传统 RBAC 建模, 单个用户关联多个角色, 角色有:
- 用户登录后台(
User检索数据) - 判断用户是最高管理员或者普通管理员( 只有最高管理员默认拥有所有权限 )
- 获取关联角色|分组(
Group|Role检索关联 ) - 通过角色|分组关联获取权限(
Auth检索菜单和接口权限 ) - 通过角色|分组关联获取渠道(
Channel检索关联渠道 ) - 通过角色|分组关联获取服务器(
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 ='用户-服务器关联对应服务器';
所以在登录的时候, 需要按照以下流程处理:
- 检索用户:
SELECT id FROM user WHERE username = 'meteorcat' LIMIT 1 - 获取分组|角色:
SELECT group_id FROM user_group_link WHERE uid = xxx - 获取权限:
SELECT autu_id FROM user_auth_link WHERE group_id IN(xxx,yyy) - 获取渠道:
SELECT channel_id FROM user_channel_link WHERE group_id IN(xxx,yyy) - 获取服务器:
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, 最后返回定义 response 的 dto 等.