后台目录:
本项目后台是由SpringBoot
框架为基础进行开发的一款Java
项目。
通过上述目录图,大致能分为三层:
当然除了大致三层还有一些工具类、配置、缓存和aop等技术的加持。
article表:
DROP TABLE IF EXISTS `article`; CREATE TABLE `article` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量', `create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间', `summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介', `title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题', `view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量', `weight` int(0) NOT NULL COMMENT '是否置顶', `author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id', `body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id', `category_id` int(0) NULL DEFAULT NULL COMMENT '类别id', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1405916999732707331 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
从表结构中我们能知道id
字段是自增不为空,且使用了BTREE索引
。
补充:
B-TREE索引
B-TREE索引的特点
B-TREE索引使用场景
article_body表:
DROP TABLE IF EXISTS `article_body`; CREATE TABLE `article_body` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `article_id` bigint(0) NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `article_id`(`article_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1405916999854342147 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
本表共有:
主键为ID
,然后给article_id
增加BTREE
索引。
article_tag表:
DROP TABLE IF EXISTS `article_tag`; CREATE TABLE `article_tag` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `article_id` bigint(0) NOT NULL, `tag_id` bigint(0) NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `article_id`(`article_id`) USING BTREE, INDEX `tag_id`(`tag_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1405916999787233282 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
本表有:
设置article
和tag_id
为BTREE
索引。
category表:
DROP TABLE IF EXISTS `category`; CREATE TABLE `category` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
本表有:
comment表:
DROP TABLE IF EXISTS `ms_comment`; CREATE TABLE `ms_comment` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `create_date` bigint(0) NOT NULL, `article_id` int(0) NOT NULL, `author_id` bigint(0) NOT NULL, `parent_id` bigint(0) NOT NULL, `to_uid` bigint(0) NOT NULL, `level` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `article_id`(`article_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1405209691876790275 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
本表有:
user表:
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号', `admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员', `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像', `create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间', `deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除', `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱', `last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间', `mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号', `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称', `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐', `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1404448588146192387 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
tag表:
DROP TABLE IF EXISTS `tag`; CREATE TABLE `tag` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `tag_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
通过上图我们知道每一个Controller
对应着前端页面相对应的请求功能。
注:本项目的所有代码都开源在Gitee上,有需要可以去自己看一下哦。
接下来我们举例看一下一些Controller
的功能。
ArticleController:
//json数据交互 @RestController @RequestMapping("articles") public class ArticleController { @Autowired ArticleService articleService; /** * 首页文章列表 * @param pageParams * @return */ //切面注解 @LogAnnotation(module = "文章",operation = "获取文章列表") //放入缓存 @Cache(expire = 5 * 60 * 1000,name = "ListArticle") @PostMapping public Result listArticle(@RequestBody PageParams pageParams){ return articleService.listArticle(pageParams); } /** * 首页最热文章 * @return */ @PostMapping("hot") @Cache(expire = 5 * 60 * 1000,name = "hot_article") public Result hotArticle(){ int limit = 5; return articleService.hotArticle(limit); } @PostMapping("new") @Cache(expire = 5 * 60 * 1000,name = "new_article") public Result newArticle(){ int limit = 5; return articleService.newArticles(limit); } /** * 首页 文章归档 * @return */ @PostMapping("listArchives") public Result listArchives(){ return articleService.listArchives(); } @PostMapping("view/{id}") public Result findArticleById(@PathVariable("id") Long articleId){ return articleService.findArticleById(articleId); } // @RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的); // 而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。 @PostMapping("publish") public Result publish(@RequestBody ArticleParam articleParam){ return articleService.publish(articleParam); }
Service层也就是来处理Controller层的一些请求。
我们接着上面的
ArticleServiceImpl:
@Service public class ArticleServiceImpl implements ArticleService { //通过注解来调用其他的Mapper和service层 @Autowired ArticleMapper articleMapper; @Autowired private SysUserService sysUserService; @Autowired private TagService tagService; @Autowired private CategoryService categoryService; @Override public Result listArticle(PageParams pageParams) { Pagepage = new Page<>(pageParams.getPage(),pageParams.getPageSize()); IPage articleIPage = this.articleMapper.listArticle(page,pageParams.getCategoryId(),pageParams.getTagId(),pageParams.getYear(),pageParams.getMonth()); return Result.success(copyList(articleIPage.getRecords(),true,true)); } @Override public Result hotArticle(int limit) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByDesc(Article::getViewCounts); queryWrapper.orderByDesc(Article::getId,Article::getTitle); //"limit"字待串后要加空格,不要忘记加空格,不然会把数据拼到一起 queryWrapper.last("limit "+limit); List articles = articleMapper.selectList(queryWrapper); return Result.success(copyList(articles,false,false)); } @Override public Result newArticles(int limit) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByDesc(Article::getCreateDate); queryWrapper.select(Article::getId,Article::getTitle); queryWrapper.last("limit "+limit); //select id,title from article order by create_date desc limit 5 List articles = articleMapper.selectList(queryWrapper); return Result.success(copyList(articles,false,false)); } private List copyList(List records,boolean isTag,boolean isAuthor) { ArrayList articleVoList = new ArrayList<>(); for (Article record : records) { articleVoList.add(copy(record,isTag,isAuthor,false,false)); } return articleVoList; } private List copyList(List records, boolean isTag, boolean isAuthor,boolean isBody) { List articleVoList = new ArrayList<>(); for (Article record : records) { articleVoList.add(copy(record,isTag,isAuthor,isBody,false)); } return articleVoList; } private List copyList(List records, boolean isTag, boolean isAuthor,boolean isBody,boolean isCategory) { List articleVoList = new ArrayList<>(); for (Article record : records) { articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory)); } return articleVoList; } private ArticleVo copy(Article article,boolean isTag,boolean isAuthor, boolean isBody,boolean isCategory){ ArticleVo articleVo = new ArticleVo(); articleVo.setId(String.valueOf(article.getId())); BeanUtils.copyProperties(article,articleVo); articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm")); if(isTag){ Long id = article.getId(); articleVo.setTags(tagService.findTagsByArticleId(id)); } if(isAuthor){ Long authorId = article.getAuthorId(); articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname()); } if (isBody){ Long bodyId = article.getBodyId(); articleVo.setBody(findArticleBodyById(bodyId)); } if (isCategory){ Long categoryId = article.getCategoryId(); articleVo.setCategory(categoryService.findCategoryById(categoryId)); } return articleVo; } @Override public Result listArchives() { /* 文章归档 */ List archivesList = articleMapper.listArchives(); return Result.success(archivesList); } @Autowired private ThreadService threadService; @Override public Result findArticleById(Long articleId) { /** * 1. 根据id查询 文章信息 * 2. 根据bodyId和categoryid 去做关联查询 */ Article article = this.articleMapper.selectById(articleId); ArticleVo articleVo = copy(article, true, true,true,true); //查看完文章了,新增阅读数,有没有问题呢? //查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低 // 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作 //线程池 可以把更新操作 扔到线程池中去执行,和主线程就不相关了 //threadService.updateArticleViewCount(articleMapper,article); threadService.updateArticleViewCount(articleMapper,article); return Result.success(articleVo); } @Autowired private ArticleTagMapper articleTagMapper; @Override @Transactional public Result publish(ArticleParam articleParam) { //注意想要拿到数据必须将接口加入拦截器 SysUser sysUser = UserThreadLocal.get(); /** * 1. 发布文章 目的 构建Article对象 * 2. 作者id 当前的登录用户 * 3. 标签 要将标签加入到 关联列表当中 * 4. body 内容存储 article bodyId */ Article article = new Article(); article.setAuthorId(sysUser.getId()); article.setCategoryId(Long.parseLong(articleParam.getCategory().getId())); article.setCreateDate(System.currentTimeMillis()); article.setCommentCounts(0); article.setSummary(articleParam.getSummary()); article.setTitle(articleParam.getTitle()); article.setViewCounts(0); article.setWeight(Article.Article_Common); article.setBodyId(-1L); //插入之后 会生成一个文章id(因为新建的文章没有文章id所以要insert一下 //官网解释:"insart后主键会自动'set到实体的ID字段。所以你只需要"getid()就好 // 利用主键自增,mp的insert操作后id值会回到参数对象中 //https://blog.csdn.net/HSJ0170/article/details/107982866 this.articleMapper.insert(article); //tags List tags = articleParam.getTags(); if (tags != null) { for (TagVo tag : tags) { ArticleTag articleTag = new ArticleTag(); articleTag.setArticleId(article.getId()); // System.out.println("这个是tag:"+tag); articleTag.setTagId(Long.parseLong(tag.getId())); this.articleTagMapper.insert(articleTag); } } //body ArticleBody articleBody = new ArticleBody(); articleBody.setContent(articleParam.getBody().getContent()); articleBody.setContentHtml(articleParam.getBody().getContentHtml()); articleBody.setArticleId(article.getId()); articleBodyMapper.insert(articleBody); //插入完之后再给一个id article.setBodyId(articleBody.getId()); //MybatisPlus中的save方法什么时候执行insert,什么时候执行update //只有当更改数据库时才插入或者更新,一般查询就可以了 articleMapper.updateById(article); ArticleVo articleVo = new ArticleVo(); articleVo.setId(String.valueOf(article.getId())); return Result.success(articleVo); } private CategoryVo findCategory(Long categoryId) { return categoryService.findCategoryById(categoryId); } //构建ArticleBodyMapper @Autowired private ArticleBodyMapper articleBodyMapper; private ArticleBodyVo findArticleBodyById(Long bodyId) { ArticleBody articleBody = articleBodyMapper.selectById(bodyId); ArticleBodyVo articleBodyVo = new ArticleBodyVo(); articleBodyVo.setContent(articleBody.getContent()); return articleBodyVo; } }
服务层是需要调用其他服务的功能和DAO层的一个处理层,通过上诉代码我们能大概知道服务层是很关键的一层。处理很多相对应的接口功能。
DAO层也就是对数据库进行操作的一个阶层,一般我们会称为Mapper层来,会使用Mybatis或者Mybatis-plus进行操作。
本次项目使用的是MyBatis-Pluss。
例如:
ArticleMapper:
public interface ArticleMapper extends BaseMapper{ List listArchives(); IPage listArticle(Page page , Long categoryId, Long tagId, String year, String month ); }
incr
自增,使用定时任务 定时把数据固话到数据库当中
以上总结的10大高频问题,均来自网友的面试问题分享。
大家做完一个项目之后,一定要去细扣一两个模块,并在面试中与面试官进行深入的交流。
比如说登录,可以思考一下登录具体的流程,前后端如何执行步骤。
比如一些电商类的分布式锁,是如何实现的?分布式事务等?这些均可以细致去思考准备等。
通过自己具体介绍项目中的一两个模块,面试官就会对你有比较深入的了解,这样给你的面评就会比较好。
当然在项目中可能还会引出一些其他的内容,顺延可能就到八股文环节了~
如果是实现的比较简单,没有使用什么中间件,只有增删改查,就会针对表的设计,一些模块的设计思路,还有场景问题,大多是那些你没有使用的中间件解决的问题:问如果很多用户访问你的主页,你会怎么办(这种高并发的问题是使用中间件解决的,你没用到,看你能不能很好的回答上来怎么解决)
其实大家做的项目,不管是什么类型,面试官更多关注的是通过这个项目你学到了什么,有什么收获,有什么自己的思考等,这些才是更重要的。
强烈建议大家好好去看看推送的项目在面试中如何准备的第一期推文,里面包含了10个非常非常高频的问题。
尤其是自己在项目遇到最大困难或者问题是什么?是怎么思考和解决的?
很多朋友可能会说,这个项目是跟着视频和文档一步步来的,似乎也没遇到很大的问题。
你可以这么回答(提供两个点,其他的大家可以发散一下思维)
我在做xx项目的时候,可能遇到的最大的问题就是xx技术的问题,在处理xx模块的时候,对xx技术的使用不太熟练等。
再或者是一些细节的错误等,如Redis连接不上SpringBoot等,或者虚拟机配置网关错误等。
以上只是两个方面,仅供参考,一定要加入自己的思考!
论坛类项目
今天给大家分享一下论坛类项目的高频问题。
做论坛类项目的朋友也比较多,如仿牛客论坛、仿CSDN、仿博客园等。
这类项目主要涉及到文章或者帖子的发布,所以更多的面试问题是围绕这些实际问题来提问的。
通过一些面经问题和实际的论坛类项目的背景,整理出下面10个高频的项目问题。
论10大高频问题汇总
上述问答我会抽时间慢慢补充的哦,希望能够对大家有所帮助。