01 整体介绍
背景
这个项目诞生的背景和企业内生的需求不太一样,主要是某一天二哥说,“我们一起搞事吧”, 楼仔问,“搞什么”,然后这个项目的需求就来了
言归正传,我们主要的目的是希望打造一个切实可用的项目,依托于这个项目,将java从业者所用到的技术栈真实的展现出来,对于经验不是那么足的小伙伴,可以在一个真实的系统上,理解到自己学习的知识点是如何落地的,同时也能真实的了解一个项目是从0到1实现的全过程
系统模块介绍
系统架构
基于社区系统的分层特点,将整个系统架构划分为展示层,应用层,服务层,如下图
展示层
其中展示层主要为用户直接接触的视图层,基于用户角色,分别提供为面向普通用户的前台与面向管理员的后台
前台web
- 采用Thymleaf模板引擎进行视图渲染
- 对于不关心前端技术栈的小伙伴相对友好,学习成本低,只用会基本的html,css,js即可
管理后台
- 采用成熟的前后端分离技术方案
- 前端基于react成熟框架搭建
应用层
应用层,也可以称为业务层,强业务相关,其中每个划分出来的模块有较明显的业务边界,虽然在上图中区分了前台、后台
但是需要注意的是,后台也是同样有文章、评论、用户等业务功能的,前台与后台可使用应用主要是权限粒度管理的差异性,对于技术派系统而言,我们的应用可分为:
- 文章
- 专栏
- 评论
- 用户
- 收藏
- 订阅
- 运营
- 审核
- 类目标签
- 统计
服务层
我们将一些通用的、可抽离业务属性的功能模块,沉淀到服务层,作为一个一个的基础服务进行设计,比如计数服务、消息服务等,通常他们最大特点就是独立与业务之外,适用性更广,并不局限在特定的业务领域内,可以作为通用的技术方案存在
在技术派的项目设计中,我们拟定以下基础服务
- 用户权限管理 (auth)
- 消息中心 (mq)
- 计数 (redis)
- 搜索服务 (es)
- 推荐 (recommend)
- 监控运维 (prometheus)
平台资源层
这一层可以理解为更基础的下层支撑
- 服务资源:数据库、redis、es、mq
- 硬件资源:容器,ecs服务器
术语介绍
技术派整个系统中涉及到的术语并不多,也很容易理解,下面针对几个常用的进行说明
- 用户:特指通过微信公众号扫码注册的用户,可以发布文章、阅读文章等
- 管理员:可以登录后台的特殊用户
- 文章:即博文
- 专栏:由一系列相关的文章组成的一个合集
- 订阅:专指关注用户
02 系统模块设计
针对前面技术派的业务架构拆分,技术派的实际项目划分,主要是五个模块,相反并没由将上面的每个应用、服务抽离为独立的模块,主要是为了避免过渡设计,粒度划分太细会增加整个项目的理解维护成本
这里设置五个相对独立的模块,则主要是基于边界特别清晰这一思考点进行,后续做微服务演进时,下面每个模块可以作为独立的微服务存在
用户模块
在技术派中,整个用户模块从功能角度可以分为
- 注册登录
- 权限管理(是的,权限管理也放在这里了)
- 业务逻辑
注册登录
方案设计
注册登录除了常见的用户名+密码的登录方式之外,现在也有流行的手机号+验证,第三方授权登录;我们最终选择微信公众号登录方式(其最主要的目的,相信大家也知道...)
对于个人公众号,很多权限没有;因此这个登录的具体实现,有两种实现策略
- 点击登录,登录页显示二维码 + 输入框 -> 用户关注公众号,输入 "login" 获取登录验证码 -> 在登录界面输入验证码实现登录
- 点击登录,登录页显示二维码 + 验证码 -> 用户关注公众号,将登录页面上的验证码输入到微信公众号 -> 自动登录
其中第一种策略,类似于手机号/验证码的登录方式,主要是根据系统返回的验证码来主动登录
优点:
- 代码实现简单,逻辑清晰
缺点:
- 操作流程复杂,用户需要输入两次
对于第二种策略,如果是企业公众号,是可以省略输入验证码这一步骤的,借助动态二维码来直接实现扫码登录;对于我们这种个人公众号,则需要多来一步,通过输入验证码来将微信公众号的用户与需要登录的用户绑定起来
登录工作流程如下:
库表设计
基于公众号的登录方式,看一下用户登录表的设计
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`third_account_id` varchar(128) NOT NULL DEFAULT '' COMMENT '第三方用户ID',
`user_name` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(128) NOT NULL DEFAULT '' COMMENT '密码',
`login_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '登录方式: 0-微信登录,1-账号密码登录',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
KEY `key_third_account_id` (`third_account_id`),
KEY `key_user_name` (`user_name`),
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户登录表';
注意上面的表结构设计,我们冗余了 user_name, password 用户名密码的登录方式,主要是给管理员登录后台使用
用户首次登录之后,会在user表中插入一条数据,主要关注 third_account_id 这个字段,它记录的是微信开放平台返回的唯一用户id
权限管理
权限管理会分为两块:用户身份识别 + 鉴权
方案设计
用户身份识别:
现在用户的身份识别有非常多的方案,我们现在采用的是最基础、历史最悠久的方案,cookie + session 方式(后续会迭代为分布式session + jwt)
整体流程:
- 用户登录成功,服务器生成sessionId -> userId 映射关系
- 服务器返回sessionId,写到客户端的浏览器cookie
- 后续用户请求,携带cookie
- 服务器从cookie中获取sessionId,然后找到uesrId
服务内部身份传递:
另外一个需要考虑的点则是用户的身份如何在整个系统内传递? 对于一期我们采用的单体架构而言,借助ThreadLocal来实现
- 自定义Filter,实现用户身份识别(即上面的流程,从cookie中拿到SessionId,转userId)
- 定义全局上下文ReqInfoContext:将用户信息,写入全局共享的ThreadLocal中
- 在系统内,需要获取当前用户的地方,直接通过访问 ReqInfoContext上下文获取用户信息
- 请求返回前,销毁上下文中当前登录用户信息
鉴权
根据用户角色与接口权限要求进行判定,我们设计三种权限点类型
- ADMIN:只有管理员才能访问的接口
- LOGIN:只有登录了才能访问的接口
- ALL:默认,没有权限限制
我们在需要权限判定的接口上,添加上对应的权限要求,然后借助AOP来实现权限判断
- 当接口上有权限点要求时(除ALL之外)
- 首先获取用户信息,如果没有登录,则直接报403
- 对于ADMIN限制的接口,要求查看用户角色,必须为admin
库表设计
我们将用户角色信息写入用户基本信息表中,没有单独抽出一个角色表,然后进行映射,主要是因为这个系统逻辑相对清晰,没有太复杂的角色关系,因此采用了轻量级的设计方案
-- pai_coding.user_info definition
CREATE TABLE `user_info` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
`photo` varchar(128) NOT NULL DEFAULT '' COMMENT '用户图像',
`position` varchar(50) NOT NULL DEFAULT '' COMMENT '职位',
`company` varchar(50) NOT NULL DEFAULT '' COMMENT '公司',
`profile` varchar(225) NOT NULL DEFAULT '' COMMENT '个人简介',
`user_role` int(4) NOT NULL DEFAULT '0' COMMENT '0 普通用户 1 超管',
`extend` varchar(1024) NOT NULL DEFAULT '' COMMENT '扩展字段',
`ip` json NOT NULL COMMENT '用户的ip信息',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
KEY `key_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户个人信息表';
业务逻辑
在业务模块,主要说两块,一个是用户的轨迹,一个是订阅关注
订阅关注
订阅关注这块业务主要是用户可以相互关注,核心点就在于维护用户与用户之间的订阅关系
业务逻辑上没有太复杂的东西,核心就是需要一张表来记录关注与被关注情况
-- pai_coding.user_relation definition
CREATE TABLE `user_relation` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '作者用户ID',
`follow_user_id` int(10) unsigned NOT NULL COMMENT '关注userId的用户id,即粉丝userId',
`follow_state` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未关注,1-已关注,2-取消关注',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_follow` (`user_id`,`follow_user_id`),
KEY `key_follow_user_id` (`follow_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户关系表';
用户轨迹
在技术派的整体设计中,我们希望记录用户的阅读历史、关注列表、收藏列表、评价的文章列表,对于这种用户行为轨迹的诉求,我们采用设计一张大宽表的策略,其主要目的在于
- 记录用户的关键动作
- 便于文章的相关计数
接下来看一下表结构设计
-- pai_coding.user_foot definition
CREATE TABLE `user_foot` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`document_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文档ID(文章/评论)',
`document_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '文档类型:1-文章,2-评论',
`document_user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发布该文档的用户ID',
`collection_stat` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '收藏状态: 0-未收藏,1-已收藏,2-取消收藏',
`read_stat` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未读,1-已读',
`comment_stat` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '评论状态: 0-未评论,1-已评论,2-删除评论',
`praise_stat` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '点赞状态: 0-未点赞,1-已点赞,2-取消点赞',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_doucument` (`user_id`,`document_id`,`document_type`),
KEY `idx_doucument_id` (`document_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户足迹表';
我们将用户 + 文章设计唯一键,用来记录用户对自己阅读过的文章的行为,因此可以直接通过这个表获取用户的历史轨迹
同时也可以从文章的角度出发,查看被哪些用户点赞、收藏过
小结
用户模块的核心支撑在上面几块,请重点关注上面的示意图与表结构;当然用户的功能点不止于上面几个,比如基础的个人主页、用户信息等也属于用户模块的业务范畴
文章模块
我们将文章和专栏都放在一起,同样也将类目管理、标签管理等也都放在这个模块中,实际上若文章模块过于庞大,也是可以按照最开始的划分进行继续拆分的;这里放在一起的主要原因在于他们都是围绕基本的文章这一业务属性来的,可以聚合在一起
文章
文章的核心就在于发布、查看
基本的发布流程:
- 用户登录,进入发布页面
- 输入标题、文章
- 选择分类、标签,封面、简介
- 提交文章,进入待审核状态,仅用户可看详情
- 管理员审核通过,所有人可看详情
文章库表设计
考虑到文章的内容通常较大,在很多的业务场景中,我们实际上是不需要文章内容的,如首页、推荐列表等都只需要文章的标题等信息;此外我们也希望对文章做一个版本管理(比如上线之后,再修改则新生成一个版本)
因此我们对文章设计了两张表
-- pai_coding.article definition
CREATE TABLE `article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`article_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '文章类型:1-博文,2-问答',
`title` varchar(120) NOT NULL DEFAULT '' COMMENT '文章标题',
`short_title` varchar(120) NOT NULL DEFAULT '' COMMENT '短标题',
`picture` varchar(128) NOT NULL DEFAULT '' COMMENT '文章头图',
`summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章摘要',
`category_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '类目ID',
`source` tinyint(4) NOT NULL DEFAULT '1' COMMENT '来源:1-转载,2-原创,3-翻译',
`source_url` varchar(128) NOT NULL DEFAULT '1' COMMENT '原文链接',
`offical_stat` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '官方状态:0-非官方,1-官方',
`topping_stat` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '置顶状态:0-不置顶,1-置顶',
`cream_stat` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '加精状态:0-不加精,1-加精',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_title` (`title`),
KEY `idx_short_title` (`short_title`)
) ENGINE=InnoDB AUTO_INCREMENT=173 DEFAULT CHARSET=utf8mb4 COMMENT='文章表';
-- pai_coding.article_detail definition
CREATE TABLE `article_detail` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`article_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文章ID',
`version` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '版本号',
`content` longtext COMMENT '文章内容',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_article_version` (`article_id`,