简介
该项目采用微服务架构,实现了前后端分离的系统设计。在前端,我们选择了 Vue3 配合 TypeScript 和 ElementUi 框架,以提升开发效率和用户体验。而在后端,则是运用 SpringBoot 和 Mybatis-plus 进行开发,保证了系统的高效和稳定。此外,我们引入了 ElasticSearch 作为全文检索服务,以加快搜索速度和提高检索效率。同时,通过 WebSocket 技术实现了实时聊天和消息推送功能,增强了用户的互动体验。
代码下载
https://url21.ctfile.com/f/15432821-1020751544-c0e7e4?p=8418
(访问密码: 8418)
项目目录
- yanhuo-web 前段页面
- yanhuo-auth 认证服务
- yanhuo-common 公共模块,存放一些工具类或公用类
- yanhuo-platform 烟火 app 主要功能模块
- yanhuo-im 聊天模块
- yanhuo-search 搜索模块
- yanhuo-util 第三方服务模块,邮箱短信,oss 对象存储服务
- yanhuo-xo 对象存放模块
源码讲解
Controller 源码示例
首先来看 AlbumController 中的代码,主要讲解八个容易误解的方面。
-
@RestController:这是一个组合注解,它表示这个类是一个控制器,并且其中的所有方法都会返回数据而不是视图。
-
@RequestMapping(“/album”):这个注解用于将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法上。在这里,它将所有的请求映射到以/album 为前缀的 URL。
-
@Autowired:这个注解用于自动注入 Spring 容器中的 bean。在这里,它注入了一个 AlbumService 的实例,这个实例提供了专辑的增删改查操作。
-
getAlbumPageByUserId 方法:这个方法用于根据用户 ID 获取专辑列表,并支持分页。它接受三个参数:当前页(currentPage),分页数(pageSize)和用户 ID(userId)。它调用 albumService 的 getAlbumPageByUserId 方法来获取数据,并返回一个 Result<?>对象。
-
saveAlbumByDTO 方法:这个方法用于保存专辑。它接受一个 AlbumDTO 对象作为参数,并使用 ValidatorUtils.validateEntity 方法进行数据校验。然后,它调用 albumService 的 saveAlbumByDTO 方法来保存专辑,并返回一个 Result<?>对象。
-
getAlbumById 方法:这个方法用于根据专辑 ID 获取专辑。它接受一个 albumId 作为参数,并调用 albumService 的 getAlbumById 方法来获取数据,并返回一个 Result<?>对象。
-
deleteAlbumById 方法:这个方法用于根据专辑 ID 删除专辑。它接受一个 albumId 作为参数,并调用 albumService 的 deleteAlbumById 方法来删除数据,并返回一个 Result<?>对象。
-
updateAlbumByDTO 方法:这个方法用于更新专辑。它接受一个 AlbumDTO 对象作为参数,并使用 ValidatorUtils.validateEntity 方法进行数据校验。然后,它调用 albumService 的 updateAlbumByDTO 方法来更新专辑,并返回一个 Result<?>对象。
package com.yanhuo.platform.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yanhuo.common.result.Result;
import com.yanhuo.common.validator.ValidatorUtils;
import com.yanhuo.common.validator.group.AddGroup;
import com.yanhuo.common.validator.group.UpdateGroup;
import com.yanhuo.platform.service.AlbumService;
import com.yanhuo.xo.dto.AlbumDTO;
import com.yanhuo.xo.entity.Album;
import com.yanhuo.xo.vo.AlbumVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RequestMapping("/album")
@RestController
public class AlbumController {@AutowiredAlbumService albumService;/*** 根据用户id获取专辑* @param currentPage 当前页* @param pageSize 分页数* @param userId 用户id* @return 专辑数*/@RequestMapping("getAlbumPageByUserId/{currentPage}/{pageSize}")public Result<?> getAlbumPageByUserId(@PathVariable long currentPage, @PathVariable long pageSize,String userId){Page<Album> page = albumService.getAlbumPageByUserId(currentPage,pageSize,userId);return Result.ok(page);}/*** 保存专辑* @param albumDTO 专辑实体* @return success*/@RequestMapping("saveAlbumByDTO")public Result<?> saveAlbumByDTO(@RequestBody AlbumDTO albumDTO) {ValidatorUtils.validateEntity(albumDTO, AddGroup.class);albumService.saveAlbumByDTO(albumDTO);return Result.ok();}/*** 根据专辑id获取专辑* @param albumId 专辑id* @return 专辑实体*/@RequestMapping("getAlbumById")public Result<?> getAlbumById(String albumId) {AlbumVo albumVo = albumService.getAlbumById(albumId);return Result.ok(albumVo);}/*** 根据专辑id删除专辑* @param albumId 专辑id* @return success*/@RequestMapping("deleteAlbumById")public Result<?> deleteAlbumById(String albumId) {albumService.deleteAlbumById(albumId);return Result.ok();}/*** 更新专辑* @param albumDTO 专辑实体* @return success*/@RequestMapping("updateAlbumByDTO")public Result<?> updateAlbumByDTO(@RequestBody AlbumDTO albumDTO) {ValidatorUtils.validateEntity(albumDTO, UpdateGroup.class);albumService.updateAlbumByDTO(albumDTO);return Result.ok();}
}
Service 源码示例
下面代码是一个服务实现类,用于处理与笔记相关的业务逻辑。这个类使用了 MyBatis-Plus 框架来简化数据库操作,并使用了 Hutool 工具类库来处理 JSON 数据。下面是这段代码的详细解释:
-
NoteServiceImpl
类继承了ServiceImpl<NoteDao, Note>
,这意味着它继承了 MyBatis-Plus 提供的基本 CRUD 操作。NoteDao
是一个接口,用于定义与笔记相关的数据库操作方法。Note
是一个实体类,用于映射数据库中的笔记表。 -
类中注入了多个服务,包括用户服务、标签服务、分类服务、ES 客户端、关注服务、点赞或收藏服务以及 OSS 客户端。这些服务将在后面的方法中被使用。
-
getNoteById
方法用于根据笔记 ID 获取笔记详情。该方法首先通过getById
方法从数据库中获取笔记对象,然后将浏览次数加 1 并更新到数据库。接下来,通过用户 ID 获取用户对象,并将用户名、头像和更新时间设置到NoteVo
对象中。然后,检查当前用户是否关注了该笔记的作者,并设置NoteVo
对象的isFollow
属性。接着,检查当前用户是否点赞或收藏了该笔记,并设置NoteVo
对象的isLike
和isCollection
属性。最后,通过标签笔记关系服务获取该笔记的所有标签,并设置到NoteVo
对象中。 -
saveNoteByDTO
方法用于保存新的笔记。该方法首先从当前用户上下文中获取用户 ID,然后将传入的 JSON 字符串转换为NoteDTO
对象,并将其转换为Note
对象。然后,将用户 ID 设置到Note
对象中,并保存到数据库。接下来,更新用户的动态数量。然后,处理标签关系,将标签 ID 和笔记 ID 保存到标签笔记关系表中。然后,通过 OSS 客户端上传图片,并将图片 URL 保存到Note
对象中。最后,将Note
对象更新到数据库,并将笔记信息添加到 ES 搜索引擎中。 -
deleteNoteByIds
、updateNoteByDTO
和getHotPage
方法目前没有实现具体的业务逻辑。这段代码的主要功能是处理与笔记相关的业务逻辑,包括获取笔记详情、保存新的笔记、更新笔记等。代码中使用了 MyBatis-Plus 框架来简化数据库操作,并使用了 Hutool 工具类库来处理 JSON 数据。此外,代码中还使用了 ES 搜索引擎和 OSS 对象存储服务来处理笔记的搜索和图片存储。
package com.yanhuo.platform.service.impl;import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yanhuo.common.auth.AuthContextHolder;
import com.yanhuo.common.exception.YanHuoException;
import com.yanhuo.common.result.Result;
import com.yanhuo.common.utils.ConvertUtils;
import com.yanhuo.platform.client.EsClient;
import com.yanhuo.platform.client.OssClient;
import com.yanhuo.platform.service.*;
import com.yanhuo.xo.dao.NoteDao;
import com.yanhuo.xo.dto.NoteDTO;
import com.yanhuo.xo.entity.*;
import com.yanhuo.xo.vo.NoteSearchVo;
import com.yanhuo.xo.vo.NoteVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;@Service
public class NoteServiceImpl extends ServiceImpl<NoteDao, Note> implements NoteService {@AutowiredUserService userService;@AutowiredTagNoteRelationService tagNoteRelationService;@AutowiredTagService tagService;@AutowiredCategoryService categoryService;@AutowiredEsClient esClient;@AutowiredFollowerService followerService;@AutowiredLikeOrCollectionService likeOrCollectionService;@AutowiredOssClient ossClient;@Overridepublic NoteVo getNoteById(String noteId) {Note note = this.getById(noteId);note.setViewCount(note.getViewCount() + 1);User user = userService.getById(note.getUid());NoteVo noteVo = ConvertUtils.sourceToTarget(note, NoteVo.class);noteVo.setUsername(user.getUsername()).setAvatar(user.getAvatar()).setTime(note.getUpdateDate().getTime());boolean follow = followerService.isFollow(user.getId());noteVo.setIsFollow(follow);String currentUid = AuthContextHolder.getUserId();List<LikeOrCollection> likeOrCollectionList = likeOrCollectionService.list(new QueryWrapper<LikeOrCollection>().eq("like_or_collection_id", noteId).eq("uid", currentUid));if(!likeOrCollectionList.isEmpty()) {Set<Integer> types = likeOrCollectionList.stream().map(LikeOrCollection::getType).collect(Collectors.toSet());noteVo.setIsLike(types.contains(1));noteVo.setIsCollection(types.contains(3));}//得到标签List<TagNoteRelation> tagNoteRelationList = tagNoteRelationService.list(new QueryWrapper<TagNoteRelation>().eq("nid", noteId));List<String> tids = tagNoteRelationList.stream().map(TagNoteRelation::getTid).collect(Collectors.toList());if (!tids.isEmpty()) {List<Tag> tagList = tagService.listByIds(tids);noteVo.setTagList(tagList);}this.updateById(note);return noteVo;}@Transactional(rollbackFor = Exception.class)@Overridepublic String saveNoteByDTO(String noteData, MultipartFile[] files) {String currentUid = AuthContextHolder.getUserId();NoteDTO noteDTO = JSONUtil.toBean(noteData, NoteDTO.class);Note note =ConvertUtils.sourceToTarget(noteDTO, Note.class);note.setUid(currentUid);boolean save = this.save(note);if(!save){return null;}// TODO 需要往专辑中添加User user = userService.getById(currentUid);user.setTrendCount(user.getTrendCount() + 1);userService.updateById(user);List<String> tids = noteDTO.getTagList();List<TagNoteRelation> tagNoteRelationList = new ArrayList<>();String tags="";if(!tids.isEmpty()){for (String tid : tids) {TagNoteRelation tagNoteRelation = new TagNoteRelation();tagNoteRelation.setTid(tid);tagNoteRelation.setNid(note.getId());tagNoteRelationList.add(tagNoteRelation);}tagNoteRelationService.saveBatch(tagNoteRelationList);tags = tagService.listByIds(tids).stream().map(Tag::getTitle).collect(Collectors.joining(","));}Category category = categoryService.getById(note.getCid());Category parentCategory = categoryService.getById(note.getCpid());List<String> dataList;try {Result<List<String>> result = ossClient.saveBatch(files, 1);dataList = result.getData();}catch (Exception e){throw new YanHuoException("添加图片失败");}String[] urlArr = dataList.toArray(new String[dataList.size()]);String urls = JSONUtil.toJsonStr(urlArr);note.setUrls(urls);note.setNoteCover(urlArr[0]);this.updateById(note);// 往es中添加数据NoteSearchVo noteSearchVo = ConvertUtils.sourceToTarget(note, NoteSearchVo.class);noteSearchVo.setUsername(user.getUsername()).setAvatar(user.getAvatar()).setLikeCount(0L).setCategoryName(category.getTitle()).setCategoryParentName(parentCategory.getTitle()).setTags(tags).setTime(note.getUpdateDate().getTime());esClient.addNote(noteSearchVo);return note.getId();}@Overridepublic void deleteNoteByIds(List<String> noteIds) {}@Overridepublic String updateNoteByDTO(NoteDTO noteDTO) {return null;}@Overridepublic Page<NoteVo> getHotPage(long currentPage, long pageSize) {return null;}
}
Vue3 源码示例
这段代码是一个Vue3组件的模板和脚本部分,用于展示和加载关注者的动态(trends)。下面是对代码的详细解释:
模板部分(Template)
<template>
标签内定义了组件的结构。- 使用了Vue的
v-infinite-scroll
指令来实现无限滚动加载更多数据。 - 使用
v-for
指令循环渲染trendData
数组中的每个动态项。 - 每个动态项包含用户头像、用户信息、动态内容、图片和交互按钮(点赞、评论等)。
- 使用
v-if
和v-else
指令来条件渲染加载中的图片和已加载的图片。 - 事件绑定(
@click
)用于处理用户交互,如跳转到用户页面、点赞、查看动态等。
脚本部分(Script)
- 导入了必要的Vue组件和图标。
- 使用Vue 3的Composition API(
<script setup>
)定义组件的逻辑。 - 响应式变量,如
currentPage
、pageSize
、trendData
等,用于控制分页和存储数据。 handleLoad
函数,用于处理图片加载完成的事件。toUser
函数,用于导航到用户页面。getFollowTrends
函数,用于获取关注者的动态数据。loadMoreData
函数,用于加载更多动态数据。toMain
函数,用于显示动态的详细内容。close
函数,用于关闭动态详细内容的显示。refresh
函数,用于刷新动态数据。like
函数,用于处理点赞和取消点赞的操作。
12.initData
函数,用于初始化动态数据。- 在组件挂载时调用
initData
函数。
样式部分(Style)
- 使用了Less预处理器编写样式。
- 定义了
.container
、.trend-container
、.trend-item
等类,用于设置组件的布局和样式。 - 使用了
flex
布局来排列用户头像、信息和图片。 - 使用了
scoped
属性,确保样式只应用于当前组件。
<template><div class="container" v-infinite-scroll="loadMoreData"><ul class="trend-container"><li class="trend-item" v-for="(item, index) in trendData" :key="index"><a class="user-avatar"><img class="avatar-item" :src="item.avatar" @click="toUser(item.uid)" /></a><div class="main"><div class="info"><div class="user-info"><a class>{{ item.username }}</a></div><div class="interaction-hint"><span>{{ item.time }}</span></div><div class="interaction-content" @click="toMain(item.nid)">{{ item.content }}</div><div class="interaction-imgs" @click="toMain(item.nid)"><div class="details-box" v-for="(url, index) in item.imgUrls" :key="index"><el-imagev-if="!item.isLoading":src="url"@load="handleLoad(item)"style="height: 230px; width: 100%"></el-image><el-imagev-else:src="url"class="note-img animate__animated animate__fadeIn animate__delay-0.5s"fit="cover"></el-image></div></div><div class="interaction-footer"><div class="icon-item"><iclass="iconfont icon-follow-fill"style="width: 1em; height: 1em"@click="like(item.nid, item.uid, index, -1)"v-if="item.isLike"></i><iclass="iconfont icon-follow"style="width: 1em; height: 1em"@click="like(item.nid, item.uid, index, 1)"v-else></i><span class="count">{{ item.likeCount }}</span></div><div class="icon-item"><ChatRound style="width: 0.9em; height: 0.9em" /><span class="count">{{item.commentCount}}</span></div><div class="icon-item"><More style="width: 1em; height: 1em" /></div></div></div></div></li></ul><div class="feeds-loading"><Refresh style="width: 1.2em; height: 1.2em" color="rgba(51, 51, 51, 0.8)" /></div><FloatingBtn @click-refresh="refresh"></FloatingBtn><Mainv-show="mainShow":nid="nid"class="animate__animated animate__zoomIn animate__delay-0.5s"@click-main="close"></Main></div>
</template>
<script lang="ts" setup>
import { ChatRound, More, Refresh } from "@element-plus/icons-vue";
import { ref } from "vue";
import { getFollowTrendPage } from "@/api/follower";
import { formateTime } from "@/utils/util";
import FloatingBtn from "@/components/FloatingBtn.vue";
import Main from "@/pages/main/main.vue";
import type { LikeOrCollectionDTO } from "@/type/likeOrCollection";
import { likeOrCollectionByDTO } from "@/api/likeOrCollection";
import { useRouter } from "vue-router";const router = useRouter();
const currentPage = ref(1);
const pageSize = ref(5);
const trendData = ref<Array<any>>([]);
const trendTotal = ref(0);
const topLoading = ref(false);
const mainShow = ref(false);
const nid = ref("");
const likeOrCollectionDTO = ref<LikeOrCollectionDTO>({likeOrCollectionId: "",publishUid: "",type: 0,
});const handleLoad = (item: any) => {item.isLoading = true;
};const toUser = (uid: string) => {router.push({ name: "user", state: { uid: uid } });
};const getFollowTrends = () => {getFollowTrendPage(currentPage.value, pageSize.value).then((res) => {const { records, total } = res.data;console.log(records, total);records.forEach((item: any) => {item.time = formateTime(item.time);trendData.value.push(item);});trendTotal.value = total;});
};const loadMoreData = () => {currentPage.value += 1;getFollowTrends();
};const toMain = (noteId: string) => {nid.value = noteId;mainShow.value = true;
};const close = (nid: string, isLike: boolean) => {const index = trendData.value.findIndex((item) => item.nid === nid);trendData.value[index].isLike = isLike;mainShow.value = false;
};const refresh = () => {let scrollTop =window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;const clientHeight =window.innerHeight ||Math.min(document.documentElement.clientHeight, document.body.clientHeight);if (scrollTop <= clientHeight * 2) {const timeTop = setInterval(() => {document.documentElement.scrollTop = document.body.scrollTop = scrollTop -= 100;if (scrollTop <= 0) {clearInterval(timeTop);topLoading.value = true;setTimeout(() => {currentPage.value = 1;trendData.value = [];getFollowTrends();topLoading.value = false;}, 500);}}, 10); //定时调用函数使其更顺滑} else {document.documentElement.scrollTop = 0;topLoading.value = true;setTimeout(() => {currentPage.value = 1;trendData.value = [];getFollowTrends();topLoading.value = false;}, 500);}
};const like = (nid: string, uid: string, index: number, val: number) => {likeOrCollectionDTO.value.likeOrCollectionId = nid;likeOrCollectionDTO.value.publishUid = uid;likeOrCollectionDTO.value.type = 1;likeOrCollectionByDTO(likeOrCollectionDTO.value).then(() => {trendData.value[index].isLike = val == 1;trendData.value[index].likeCount += val;});
};const initData = () => {getFollowTrends();
};initData();
</script><style lang="less" scoped>
.container {flex: 1;padding: 0 24px;padding-top: 72px;width: 67%;height: 100vh;margin: 0 auto;.feeds-loading {margin: 3vh;text-align: center;}.trend-container {.trend-item {display: flex;flex-direction: row;padding-top: 24px;max-width: 100vw;.user-avatar {margin-right: 24px;flex-shrink: 0;.avatar-item {width: 48px;height: 48px;display: flex;align-items: center;justify-content: center;cursor: pointer;border-radius: 100%;border: 1px solid rgba(0, 0, 0, 0.08);object-fit: cover;}}.main {flex-grow: 1;flex-shrink: 1;display: flex;flex-direction: row;padding-bottom: 12px;border-bottom: 1px solid rgba(0, 0, 0, 0.08);.info {flex-grow: 1;flex-shrink: 1;.user-info {display: flex;flex-direction: row;align-items: center;font-size: 16px;font-weight: 600;margin-bottom: 4px;a {color: #333;}}.interaction-hint {font-size: 14px;color: rgba(51, 51, 51, 0.6);margin-bottom: 8px;}.interaction-content {display: flex;font-size: 14px;color: #333;margin-bottom: 12px;line-height: 140%;cursor: pointer;}.interaction-imgs {display: flex;.details-box {width: 25%;border-radius: 4px;margin: 8px 12px 0 0;cursor: pointer;.note-img {width: 100%;height: 230px;display: flex;border-radius: 4px;align-items: center;justify-content: center;cursor: pointer;object-fit: cover;}}}.interaction-footer {margin: 8px 12px 0 0;padding: 0 12px;display: flex;justify-content: space-between;align-items: center;.icon-item {display: flex;justify-content: left;align-items: center;color: rgba(51, 51, 51, 0.929);.count {margin-left: 3px;}}}}}}}
}
</style>