Memory 缘忆交友社区-开发文档

本文最后更新于:1 年前

岁月悠悠,带走了青春的容颜,却留下了更加成熟与深邃的灵魂。

介绍

  • 项目概述:对项目的整体简介,包括项目的目的、背景和主要功能。
  • 目标受众:定义项目的受众群体,如用户、开发人员、测试人员等。

技术栈

前端技术栈

  • Ant Design Vue

  • @vue/cli 脚手架

  • Vant UI 组件库

  • TypeScript

  • Axios 请求库

后端

  • Java SpringBoot 2.7.x 框架
  • MySQL 数据库
  • MyBatis-Plus
  • MyBatis X 自动生成
  • Redis 缓存(Spring Data Redis 等多种实现方式)
  • Redisson 分布式锁
  • Easy Excel 数据导入
  • Spring Scheduler 定时任务
  • Swagger + Knife4j 接口文档
  • Gson:JSON 序列化库
  • 相似度匹配算法

数据库:列出用于存储用户数据、队伍数据和博客数据的数据库系统,如 MySQL、MongoDB 等。

通信技术:列出用于实现即时聊天功能的通信技术,如 WebSocket、Socket.io 等。

功能模块

  • 用户认证模块
    • 用户注册功能:包括用户的基本信息和账户创建。
    • 用户登录功能:验证用户提供的用户名和密码。
    • 用户权限管理:根据用户角色和权限限制用户的操作范围。
  • 队伍管理模块
    • 创建队伍功能:允许用户创建新的队伍,并添加队伍的相关信息。
    • 加入队伍功能:允许用户查找并加入现有的队伍。
  • 好友管理模块
    • 查找好友功能:用户可以查找其他用户并添加为好友。
    • 好友聊天功能:提供好友之间的即时文本聊天功能。
  • 队内聊天模块
    • 队伍内部聊天功能:允许队伍成员之间进行实时文本交流。
  • 用户推荐模块
    • 标签匹配功能:根据用户的标签来为用户推荐与他相似度最高的其他用户。
  • 博客模块(日后增加)
    • 发布博客功能:用户可以发布自己的博客。
    • 点赞、收藏功能:用户可以对博客进行点赞和收藏。
    • 博客评论功能:用户可以对博客进行评论。

用户模块

用户登录/注册

查看用户信息

添加好友

  • 添加好友后,数据库应该同时保存两条记录:我是你的好友,你是我的好友

查看在线好友信息

  • 查看在线好友列表
    • 列表展示在线好友信息:昵称、头像、是否在线
    • 如果不在线,显示最近在线时间(或显示多久前在线:X 天前、X 小时前、X 个月前)
    • 可点击私聊
    • 提供修改好友备注功能

好友私聊

  • 如何实现好友间私聊?

  • 这其中要思考的场景有:发送消息、接收消息、查看消息

  • 在前后端搭建好 socket 环境、前端能够发送消息,服务端能正常转发消息到指定用户 id的前提下,我们考虑如何实现好友间私聊

  • 即:用户间通信
    • 用户发送消息(message),消息中携带的内容:发送者 id,目标 id,消息内容,发送时间
    • 服务器收到消息,按以发送者 id 为 key 值,将 message 存储至 Redis 中,再转发 message 到目标用户
    • 目标用户在线时,可以立刻获取到发来的 message,如若离线时,则需查看 Redis 中的存放的消息
  • 本来想着服务器如何给指定用户 sid 转发消息,好像不太好实现

  • 在客户端尝试连接服务端 socket,并成功连接后,服务端的 webSocketSet 存放了 在线连接用户的信息

  • 按现在的情况,服务端只能校验该消息是哪个用户所发的,即校验 webSocketSet 中存放的 sid,能否在已连接用户中找到发送消息的用户,并选择转发消息

  • 那只好是服务器同意转发至客户端,客户端根据 sendId 和 receiveId 选择是否展示

用户列表展示

  • 用户不应该直接看到用户列表,用户可以添加好友,而添加好友的途径有三个:
    • 通过每日推荐,相似度匹配算法,每周给用户推荐兴趣爱好相似的用户,可以添加为好友畅聊
    • 通过博文,在博客中,用户可以自由发表博文,对博主感兴趣即可申请添加好友
    • 在搜索界面,根据不同的搜索参数:userAccoount,tags(用户标签),搜索到条件匹配的匹配用户

队伍模块

队伍创建问题

  • 用户如何创建队伍,应该有以下场景:
    • 在大厅发起组队,用户即可在组队大厅搜索到组队队伍(目前只想到这个方式)
    • 创建队伍后,队长可以进行的操作:
      • 修改队伍信息
      • 退出队伍、解散队伍
      • 分享队伍,可以向好友发送组队邀请

队伍加入问题

私密队伍如何加入?目前想到的场景:

  • 当用户创建私密队伍后,其他用户将无法直接在大厅中看到该队伍,并申请加入。私密队伍的加入场景可以设计如下:

    • 邀请制加入:队长可以通过私信、邮件或其他方式向指定的用户发送邀请,邀请他们加入私密队伍。通过邀请链接或者邀请码,被邀请用户可以直接加入队伍。

    • 好友推荐:队长可以在队伍详情中选择通过好友推荐的方式,将私密队伍信息分享给指定好友。被推荐用户可以看到私密队伍,并有选项选择是否加入。

    • 特殊条件加入:队长可以设置一些特殊的条件,供用户满足后才能加入私密队伍。例如,要求用户达到一定的等级、完成特定任务或通过一定的测试等。

  • 在这些场景下,你可以使用数据库来存储队伍的私密信息,例如邀请码或特殊条件。当用户申请加入私密队伍时,你可以将用户输入的邀请码或满足的特殊条件与数据库中存储的信息进行匹配验证。如果验证通过,用户就可以成功加入私密队伍。

  • 申请加入队伍:

    • 点击队伍标签,可展示更多信息(抽屉实现),可申请加入队伍
    • 传入参数未:该队伍信息,后台处理申请请求,处理逻辑如下
      • 该队伍未满员
      • 该成员不再该队伍中
      • 更新队伍已加入人数
      • 更新 user-team 表关系,添加记录

队伍退出

队内私聊

队伍推荐

  • 即队伍成员可分享该队伍的信息,加大队伍曝光率

博客模块

  • 这个网站首页就应该是博客模块
  • 创建博文表

博文展示

  • 可筛选最新博文、最热门博文、

  • 展示所有博文

    • 博文卡片展示信息:标题、简介、标签、发布时间、点赞量、收藏量、评论量、作者头像
    • 点击进入博文详情页:与博文卡片展示内容相同,另附博文详细内容

发表博文

  • 用户可在博文编辑页,编辑博文:
    • 标题、简介、标签、内容
    • 点击发表,发表博文;点击存入草稿,存入草稿

点赞博文

收藏博文

评论博文

  • 核心

界面设计

  • 用于展示项目各个功能模块的界面设计,包括页面布局、交互流程和 UI 元素等。
  • 主页
  • 用户注册页:实现用户注册
  • 用户登录页:实现用户登录
  • 用户列表页:展示在线用户列表
  • 用户信息页:展示用户信息
  • 队伍列表页:展示队伍列表
  • 队内聊天
  • 好友聊天
  • 博客列表页:展示已发布的博客
  • 个人博客页:个人发布的博客展示

开发计划

  • 列出项目开发的计划和里程碑,包括每个功能模块的开发时间和测试时间。

Day1

  • 实现主页页面的简单开发 的 ✔ (2023/09/11 晚)
  • 在线用户列表页、队伍信息页 ✔

Day2

  • 用户登录页

    • 简单的实现了登录页的开发,用户需输入账户密码进行登录,这样作双向绑定:✔ (2023/09/10 早)
    1
    2
    3
    4
    5
    6
    7
    <a-form-item
    label="用户名"
    name="username"
    :rules="[{ required: true, message: '请输入用户名' }]"
    >
    <a-input :placeholder="'请输入用户名'" v-model:value="userAccount"/>
    </a-form-item>
    1
    const userAccount = ref("");
    • 登录成功之后跳转至主页
  • 优化了主页页面显示

    • 通过监听 tab 标签页的变化,发起相应的请求 ✔
    • 用户登录后,展示头像、昵称; ✔
    • 用户未登录,提示登录,可点击按钮跳转至登录页面 ✔
  • 新增展示所有队伍功能,展示队伍列表 ✔

    • 默认仅展示公开队伍,可开启展示加密队伍 ✔
    • 前端监听开关状态,发送带参(isSecret)请求,是否需要获取加密队伍: ✔
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 开关 队伍状态改变
    const checked = ref<boolean>(false);
    // 监听开关状态变化
    watchEffect(() => {
    if (checked.value) {
    getTeamList(true);
    } else {
    getTeamList(false);
    }
    });
    • 后台校验逻辑
    1
    2
    3
    4
    5
    6
    if (!teamList.getIsSecret()) {
    tqw.eq("status", TeamStatusEnum.PUBLIC.getValue());
    } else {
    tqw.eq("status", TeamStatusEnum.PUBLIC.getValue())
    .or().eq("status", TeamStatusEnum.SECRET.getValue());
    }
  • 队伍列表页面应该展示的信息有:
    • 队伍名、队伍描述、队伍名片(牌面)✔
    • 队伍已加入人数,可供用户判断是否满员(使用进度条实现),若满员,则显示已满员(爆红)✂
    1
    2
    3
    4
    <div>
    <a-slider id="test" v-model:value="item.joinNum" :max="item.maxNum" disabled/>
    <span>{{item.joinNum}}/{{item.maxNum}}</span>
    </div>
    • 加标签,显示该用户是否已加入该队伍;
      • 没加入,可加入
      • 已加入,可进入队伍聊天
  • 队伍卡片可点击进入,队伍信息展示页:(使用抽屉组件实现)
    • Ant Design Vue (antdv.com)
    • 队伍名、队伍描述 ✔
    • 队伍已加入成员的头像排列(使用头像组件实现)、队伍已加入人数 ✔

问题解决

  • 如何实现:点击队伍卡片,从右侧弹出抽屉,展示该队伍的详细信息
  • 进行如下绑定(每个 card 绑定一个 drawer(2023/9/10 晚)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<a-list-item>
<a-card hoverable class="teamInfoCard" @click="showDrawer(item.id)">
<template #cover>
<img alt="example" :src="item.imgUrl"/>
</template>

<a-card-meta :title="item.name">
<template #description>{{ item.description }}</template>
</a-card-meta>
</a-card>

<a-drawer
:key="item.id"
:title="item.name"
placement="right"
:closable="false"
v-model:visible="visible[item.id]"
:after-visible-change="afterVisibleChange"
>
</a-drawer>
</a-list-item>
1
2
3
4
5
// 抽屉展示
const visible = ref({});
const showDrawer = (itemId: any) => {
visible.value[itemId] = true;
};

Day3

  • 实现用户间互相添加好友,可在“我的好友”中查看 (2023/09/11 晚)
  • 用户申请加入队伍,可在“我的队伍”中查看
  • 用户创建队伍、退出队伍
  • 在线用户列表中,排除当前用户:
1
2
QueryWrapper<User> lqw = new QueryWrapper<>();
lqw.ne("id", loginUser.getId());
  • 添加用户信息标签展示

    • 点击可展示更多信息(抽屉组件实现)
    • 可申请添加为好友
      • 申请添加好友时,请求参数为该好友的 id,传回后端将登录用户和好友 id,好友表新增一条记录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 申请添加好友
    const addFriends = (friend: userInfo) => {
    myAxios
    .post("/friends/add", {
    id: friend.id,
    })
    .then((res) => {
    message.success("成功发送好友申请");
    })
    .catch(() => {
    console.log("加入失败");
    });
    };
    • 可与该队友聊天
  • 展示我的好友列表

1
2
fqw.select("friend_id");
List<Friends> friendsList = list(fqw);
1
2
3
4
5
6
7
8
根据id查询道好友信息
for (Friends friends : friendsList) {
Long friendId = friends.getFriendId();
uqw.eq("id", friendId);

User one = userService.getOne(uqw);
userList.add(one);
}
  • 这里添加 fqw.select(“friend_id”); 后,friend 为 null,待解决
1
2
3
4
5
6
7
8
9
10
11
12
create table friends
(
id bigint auto_increment comment 'id'
primary key,
user_id bigint null comment '用户id',
friend_id bigint null comment '好友id',
note varchar(8) null comment '好友备注',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
is_delete tinyint(1) default 0 not null comment '是否删除'
)
comment '好友表';
  • 添加好友信息逻辑:

    • 传递参数:该好友信息、申请用户信息
    • 该用户好友数未上限(目前没有上限,日后优化
    • 更新好友表,添加一条记录
  • 优化队伍列表展示

    • 目前还没有实现搜索队伍、搜索用户功能,展示队伍列表逻辑如下:
      • 默认展示公开队伍,可选择展示加密队伍,大厅不支持用户直接加入私密队伍
      • 显示已加入的队伍,排列顺序考前,不允许用户加入该队伍
      • 加入加密队伍需要输入密码,加入公开队伍则无

Day4

  • 在博客系统开发前,可以考虑将后端代码分模块

  • 添加用户登录状态字段(2023/09/12 早)
1
is_online     int                                    not null comment '是否在线'
  • 优化登录/退出登录体验
    • 登录后

      • 后端,存储用户信息到 session 中,设置用户状态为在线(ONLINE),并返回该信息到前端
      1
      request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
      • 前端:前端存储登录用户信息到变量 currentUser 中,用于其他业务逻辑中的校验当前登录用户权限
      1
      const currentUser = ref({});
    • 登出后

      • 后端,删除 session 中的登录用户信息,设置用户状态为在线(OFFLINE
      1
      request.getSession().removeAttribute(USER_LOGIN_STATE);
      • 前端,currentUser 置为空
  • 好友在线列表,展示好友的在线状况

1
2
3
4
5
6
7
8
9
10
11
<div v-if="item.isOnline === 1">
<a-tag color="processing" >
在线
</a-tag>
</div>

<div v-if="item.isOnline === 0">
<a-tag color="default" >
下线
</a-tag>
</div>
  • 想解决同一个浏览器只能登录一个账号的问题,但这个问题解决不了的,看看 AI 的解释:
1
2
3
4
5
6
7

存储登录用户信息在 session 中,通常是基于会话的状态管理机制。而一个浏览器不能同时登录两个用户的主要原因是浏览器的 Cookie 机制和同一域名下的会话管理。
当一个用户登录后,服务器会在浏览器中设置一个名为 Session ID 的 Cookie,用于识别会话。浏览器每次发送请求时,会自动将该 Cookie 值包括在请求头中,服务器会根据该 Cookie 值找到对应的会话信息。
由于浏览器默认只能存储一个名为 Session ID 的 Cookie,所以同一浏览器只能保持一个登录会话。当再次登录另一个用户时,服务器会更新 Session ID,并将新的 Cookie 值响应给浏览器,导致之前的会话信息失效。
即使使用 Redis 存储会话数据,也无法解决一个浏览器不能同时登录两个用户的问题。因为是浏览器自身的限制,无论使用什么方式存储会话数据,都无法绕过浏览器同一域名同一时刻只能持有一个 Cookie 的特性。
如果需要在同一浏览器中同时登录多个用户,可以考虑使用不同的浏览器或使用浏览器的隐私模式或使用不同的域名或端口。或者可以使用多个浏览器会话、多个标签页或多个窗口的方式,在同一浏览器中模拟多个用户。
希望能够解答你的疑问,如果还有其他问题,请随时提问。
  • 解决方案:使用不同的浏览器或者使用浏览器的隐私模式
  • 添加好友,同时添加两条记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // 他成为我的好友
Friends friends = new Friends();
friends.setUserId(userId);
friends.setFriendId(friend.getId());
friends.setNote("我的好友");
friends.setCreateTime(new Date());
friends.setUpdateTime(new Date());
friends.setIsDelete(0);
boolean save1 = this.save(friends);

// 我成为他的好友
friends = new Friends();
friends.setUserId(friend.getId());
friends.setFriendId(userId);
friends.setNote("我的好友");
friends.setCreateTime(new Date());
friends.setUpdateTime(new Date());
friends.setIsDelete(0);
boolean save2 = this.save(friends);

// 保证原子操作
if (!save1 || !save2) {
throw new BusinessException(ErrorCode.UPDATE_ERROR, "建立好友关系失败");
}
  • 较冗长,待优化
  • 完成加入队伍逻辑实现

  • 展示我创建的队伍、我加入的队伍

    • 要明确,我创建的队伍,team 表中保存有队长 userId 字段,仅需在 team 表中,根据 userId 字段,直接查询出对应记录的 team 信息
    • 我加入的队伍,在 user_team 表中,根据 userId,查询到对应的 teamId,在 team 表中,根据 teamId 获取到 team 信息
  • 后台返回的队伍信息,应该包括队长信息,而不是队长 id(待完成)

    • 创建 teamVO,封装参数 userName,即队长昵称
    • 我封装了一个方法,传入一个 teamList,将所有的 team 转换为 teamVO,最后返回:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 转换teamList为teamVOList
    *
    * @param teamList teamList
    * @return teamVOList
    */
    public List<TeamVO> getTeamVOByTeam(List<Team> teamList) {
    List<TeamVO> teamVOList = teamList.stream().map(team -> {
    Long userId = team.getUserId();
    String userName = userService.getById(userId).getUsername();
    TeamVO teamVO = new TeamVO();

    teamVO.setId(team.getId());
    teamVO.setName(team.getName());
    teamVO.setDescription(team.getDescription());
    teamVO.setMaxNum(team.getMaxNum());
    teamVO.setUserName(userName);
    teamVO.setImgUrl(team.getImgUrl());
    teamVO.setJoinNum(team.getJoinNum());
    teamVO.setStatus(team.getStatus());
    teamVO.setExpireTime(team.getExpireTime());
    teamVO.setCreateTime(team.getCreateTime());
    teamVO.setUpdateTime(team.getUpdateTime());
    teamVO.setIsDelete(team.getIsDelete());

    return teamVO;
    }).collect(Collectors.toList());

    return teamVOList;
    }

Day5

  • 实现用户私聊,相关配置我已做好,详情可参考这篇博客:SpringBoot 集成 WebSocket 进行前后端通信_谢小涛的博客-CSDN 博客

  • 后续该功能完成后,会详细记录 (2023/09/13 早)
  • 点击进入私聊,展示聊天信息

  • 对于聊天信息的展示,还有几分疑惑:

    • 用户发送消息后,服务器转发给指定用户应该怎样实现?
    • 还是说存储到 Redis 中后,用户在私聊的聊天页面中,直接从 Redis 中查找自己与对方的消息,再展示出来呢?
    • 这些问题我还感到很迷惑,一时想不明白如何实现,待解决
  • 现在待解决的问题还很多:

    • 用户点击聊天用户,进入聊天页面,这个页面下,能够获取到双方的聊天记录

    • 发送消息,就是在添加记录,并实时展示的过程,

    • 页面的设计我有很多想法,通信功能实现,待解决

Day6

  • 成功获取到存储在 Redis 中的信息(2023/09/14 早)
1
2
3
4
5
6
7
String msgJson = (String) redisTemplate.opsForValue().get("memory:user:message:" + loginUserId);
Gson gson = new Gson();
Message message = gson.fromJson(msgJson, Message.class);
if (message == null)
return null;

return message;
  • 用户发送消息:以用户 id 为 key,选用 Hash 数据结构,存储发出的各条消息:
1
2
3
4
5
6
7
HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash();
opsForHash.put(senderMsgKey, generateMessageId(), message);
opsForHash.put(receiverMsgKey, generateMessageId(), message);
// 3.3.设置键的过期时间,单位为h
long expireTime = 2; // 设置为2hour
redisTemplate.expire(senderMsgKey, expireTime, TimeUnit.HOURS);
redisTemplate.expire(receiverMsgKey, expireTime, TimeUnit.HOURS);
  • 获取消息记录:以当前用户 id 为 key,获取存储的消息列表,返回给前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Map<Object, Object> mesEntriesJson = redisTemplate.opsForHash().entries(USER_CHAT_MESSAGE + loginUserId);
Gson gson = new Gson();
String jsonString = gson.toJson(mesEntriesJson);
Map<Object, Object> mesEntries = gson.fromJson(jsonString, new TypeToken<Map<Object, Object>>() {
}.getType());

if (mesEntries == null)
return null;

ArrayList<Message> messageList = new ArrayList<>();
Collection<Object> mesValuesJson = mesEntries.values();
for (Object mesValueJson : mesValuesJson) {
String mesValuesJsonStr = (String) mesValueJson;
Message message = gson.fromJson(mesValuesJsonStr, Message.class);
messageList.add(message);
}
  • 如何实现在用户聊天过程中,实时更新聊天记录内容呢?需解决两个问题:

    • 用户发送消息后,立刻展示在页面(发送消息给服务器,服务器存储信息到 redis 后,再立刻查询该消息,并返回到前台)
    • 对方发送消息后,立刻展示在页面
      • 这个实现还在思考中,需要前端不断查询 redis,消息发生被更新就立刻同步到前台吗?太不现实了,很浪费资源
      • 那就应该是服务器主动转发消息了
  • 本来想着服务器如何给指定用户 sid 转发消息,好像不太好实现

  • 在客户端尝试连接服务端 socket,并成功连接后,服务端的 webSocketSet 存放了 在线连接用户的信息

  • 按现在的情况,服务端只能校验该消息是哪个用户所发的,即校验 webSocketSet 中存放的 sid,能否在已连接用户中找到发送消息的用户,并选择转发消息

  • 那只好是服务器同意转发至客户端,客户端根据 sendId 和 receiveId 选择是否展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 接受客户端发来的消息
*
* @param message 消息
* @throws IOException IOException
*/
@OnMessage
public void onMessage(String message) throws IOException {
log.info("收到来自窗口" + sid + "的信息:" + message);
// 1.消息存放Redis
boolean saveMessage = saveMessage(message);
// 2.校验存放
if (!saveMessage) {
throw new BusinessException(ErrorCode.UPDATE_ERROR_REDIS, "用户信息存放失败");
}
// 3.消息转发
msgForward(message, webSocketSet, sid);
}

Day7

  • 实时双向通信终于测试成功,详情可见 踩坑记录 栏目的 **双向通信调试成功 ** (2023/09/15 早)

  • 完成用户私聊功能、完善组队功能,开发队伍聊天功能

  • 计划优化整个网站的页面效果

  • 点击好友私聊,跳转至聊天页面

    • 首先要开发一个聊天页面:聊天窗口、可支持输入聊天内容、点击发送
    • 携带好友 id 进入聊天页面,确认消息的接收方
    • 用户登录以后,就应该连接到服务器的 socket 服务了,这样既可以实时接收消息,也可以在后台接收消息、标注为未读
1
2
3
4
5
6
7
8
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
receiveMsg.value = JSON.parse(msg.data);
// 是发给自己的消息 更新聊天记录
if (currentUserId === receiveMsg.value.receiverId) {
getMesList();
}
};
  • 用户能够发送消息给指定用户,实现了只有指定用户才能接收到该消息的功能

  • 优化消息列表展示

  • Vue 前端页面实现真麻烦,难倒我了,本来获取当前页面路由很简单的,搞了半天。。

页面更新

  • 经过一周的沉淀,想明白了整个网站的页面布局

  • 点击私聊,可跳转至聊天页面

    • 本来打算这个功能是在 tab 页之间跳转的,但是 tab 组件仅支持监听 tab 页变化,获取 key 值,不支持根据 key 值,跳转 tab 页
    • 索性直接把页面大改版,做成三个主页面:
      • 用户功能页(找用户、找队伍)
      • 聊天页(私聊、队内聊天、大厅聊天
      • 博客页(博客展示、评论、点赞、收藏)

image-20230915183535885

image-20230915183544621

Day8

修改数据库

  • 这个账户和昵称我一直认为不合理

    • 统一为用户名 userAccount
    • 用户注册/登录后支持修改个人信息:绑定电话、邮箱,上传头像,修改用户名和密码
    • 不过保留也可以,用户使用唯一账户登录后,如果没有取昵称,强制弹窗要求用户为自己取一个昵称,我真聪明
  • 好友列表 点击 私聊 跳转至聊天大厅的特定聊天窗口处(2023/09/16 早)

    • 传递好友 id,聊天大厅 tab 页接收 id 参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 监听页面变化
    const goToTab = () => {
    router.push({
    name: "chat",
    path: "/chat",
    query: {
    chatUserId: 12345,
    },
    });
    };
    1
    2
    3
    4
    5
    6
    const route = useRoute();
    const chatTabName = ref({});
    // 钩子函数
    onMounted(() => {
    chatTabName.value = route.query;
    });
    1
    <a-tab-pane key="1" :tab="chatTabName.chatUserId">
  • 原本打算做这样的实现:

    • 点击好友私聊后,能够跳转至聊天大厅,并创建一个特有的 tab 标签页,用来做与该好友的聊天页面
    • 只要传递好友username 作为参数,将 tab 页命名为 username,即可创建特有的 tab 标签页
    • 但是每次只能由一个好友聊天窗口,像这样:

    image-20230916131802853

    • 这是因为每次只能追加一个 tab 页面,而退出该页面后,添加的 tab 页组件就没了,不能保存下来
    • 有关这样的问题,肯定是有解决办法的,好像有个 keep-alive 标签就是干这个的,不过不深究了
  • 现在索性改为这样实现了:

    • 直接罗列出所有的好友聊天列表,每次点击私聊后不是根据传递的参数新增一个 tab 页,而是定位到对应的标签页
  • 成功实现点击私聊后,跳转至聊天大厅中的对应聊天窗口

  • 正式着手开发双向实时通信,还有个小 BUG,我自己的消息,会在每个聊天窗口显示

  • 查找聊天消息时,应该在每个聊天窗口发出请求,请求参数为当前用户 id 和聊天对象 id,查询这些消息

  • 业务逻辑好像出问题了:

    • 现在用户给指定用户发送消息后,确实保证了只有该用户能收到消息
    • 但是收到消息如何持久化到聊天记录中呢?不能光打印一下就完事了对吧
    • 那就是要重新查询该聊天窗口的消息,并返回给前端
    • 怎么查询?根据两用户的 id 查询:你给我发的消息,我给你发的消息
  • 成功解决核心问题,基本实现了实时双向聊天通信功能 🌞🌞🌞🌞🌞

image-20230916212327320

image-20230916212506289

  • 添加用户昵称或头像,在聊天中能分辨收发双方

Day9

  • 了解 Java 并发编程,批量插入用户数据(2023/09/17 晚)
  • 每周推荐用户:

    • 相似度匹配算法
    • 根据用户标签,对比标签的相似度,得出相似度降序排行,取出前 5 条数据,即每周推荐用户为 5 个
    • 如何实现每周推荐 5 个呢?写个定时任务,只要服务器开启,每隔一周查询一次相似用户,并更新到数据库中
  • 实现用户推荐,改为了这样的实现:

    • 获取推荐用户列表

    • 如果成功从缓存中获取,则返回;否则查询数据库,更新缓存,设置 7 天过期
    • 这样也巧妙的实现了每周推荐,不需要写定时任务了

  • 解决刷新 chatPage.vue 页后,当前页登录用户获取失败的问题

  • 进入聊天大厅,自动连接服务器,提供双向实时通信服务

  • 计划开发博文分享页了,参考这个:

image-20230917230332243

  • 思考设计博文表,同时优化通信功能(2023/09/17 晚)

优化页面计划:

Day10:

  • 博客表的创建:SQL 之父
  • 博客列表展示:

    • 添加测试数据,设计简单的博客页展示
  • 优化每周推荐用户列表展示

  • 优化实时双向通信体验

  • 用户未登录时,拿取推荐列表报错,需校验当前用户是否登录成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取推荐用户
const getMatchUserList = () => {
myAxios
.get("/user/match", {
params: {
matchNum: matchNum.value,
},
})
.then((res) => {
if (res.data !== null) {
matchUserList.value = res.data.records;
}
});
};
  • 未登录,获取的推荐用户即为空,设置下为空时的页面展示即可
  • 测试成功,获取所有博文列表:

image-20230918134952276

  • 解决了 icon 图标不显示的问题,原来还要安装相关依赖才可以(看官网)
1
npm install --save @ant-design/icons-vue
  • 成功完成了页头的优化(导航栏透明、简洁、大方,且提供界面之间的来回跳转),还添加了渐变的背景色,效果如下:

image-20230918195353981

image-20230918195402432

image-20230918195407229

  • 博文发布:
    • 用户可以在博文编辑页,编写博文,并发布

Day11

  • 实现计算每周匹配用户的匹配度

    • 编辑距离算法计算编辑距离
    1
    2
    // 计算编辑距离
    long distance = AlgorithmUtils.minDistance(tagList, userTagList)
    • 根据最大编辑距离和最小编辑距离,计算匹配度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 计算匹配度
    double percentage = getPercentage(distance);

    /**
    * 计算匹配度
    *
    * @param distance 编辑距离
    * @return 匹配度
    */
    public double getPercentage(long distance) {
    // 计算匹配度的百分比
    return (1 - (distance - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE)) * 100;
    }

    • 封装 UserVO 类,添加匹配度 percentage 字段
    1
    2
    3
    4
    @Data
    public class UserVO extends User {
    private double percentage;
    }
    • userList 转换 userVOList
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /**
    * 转换 userList 为 userVOList
    *
    * @param userList userList
    * @return teamVOList
    */
    public List<UserVO> getUserVOByUser(List<User> userList, List<Double> distanceList) {
    return IntStream.range(0, userList.size())
    .mapToObj(i -> {
    User user = userList.get(i);
    double distance = distanceList.get(i);

    UserVO userVO = new UserVO();
    userVO.setPercentage(distance);
    userVO.setId(user.getId());
    userVO.setUserAccount(user.getUserAccount());
    userVO.setUsername(user.getUsername());
    userVO.setUserPassword(user.getUserPassword());
    userVO.setAvatarUrl(user.getAvatarUrl());
    userVO.setGender(user.getGender());
    userVO.setPhone(user.getPhone());
    userVO.setEmail(user.getEmail());
    userVO.setUserStatus(user.getUserStatus());
    userVO.setIsOnline(user.getIsOnline());
    userVO.setCreateTime(user.getCreateTime());
    userVO.setUpdateTime(user.getUpdateTime());
    userVO.setIsDelete(user.getIsDelete());
    userVO.setUserRole(user.getUserRole());
    userVO.setPlanetCode(user.getPlanetCode());
    userVO.setTags(user.getTags());
    userVO.setProfile(user.getProfile());

    return userVO;
    })
    .collect(Collectors.toList());
    }
  • 将匹配度百分比保留了小数点后两位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 计算匹配度
*
* @param distance 编辑距离
* @return 匹配度
*/
public double getPercentage(long distance) {
// 计算匹配度的百分比
double percentage = (1 - (distance - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE)) * 100;
// 将 double 数据保留小数点后两位,并转换为字符串
String formattedNumber = String.format("%.2f", percentage);
System.out.println(formattedNumber);
// 将String转换为Double
return Double.parseDouble(formattedNumber);
}
  • 已成功定位到相关问题:

    • 在聊天大厅刷新页面后,向后端发送获取聊天记录列表时,没有携带当前登录用户 id即 senderId = null
    • 这里发现一个问题:
      • 之前的聊天页面,只能通过点击私聊才能跳转,而跳转时,路由中是携带了聊天对象的 id
      • 在经过页面更新以后,可以直接从主页面或者博客页面跳转,这时候就没有聊天用户 id 了
      • 所以传获取消息时,receiverId 参数总为 undefined
    • 解决办法:
      • 对于私聊跳转,页面刷新,不能依赖于获取路由参数,而应该事先将变量保存,直接取到变量值即可
      • 而对于大厅跳转,则应该根据选中的 tab 标签页,获取对应 key 值
      • 应该做好tab 标签页 key 值初始化以及动态更新的工作
  • 根本解决不了,就应该在聊天大厅新增一个页面或窗口,大厅跳转进来是直接选中这个窗口的

  • 然后通过选中 tab 标签页,给 activeKey 赋值,这时候刷新页面就能停留在对应标签页上了

  • 好极了,新增文件传输助手,页面跳转直接选中该 tab 标签页:

1
2
<a-tab-pane v-model:activeKey="activeKey" :key="currentUserId" tab="文件传输助手"
@click="handleTabChange">
1
2
// tab页 key
const activeKey = ref(currentUserId);
  • 何时跳转文件传输助手页?
    • 其他大厅首次跳转,进入聊天大厅页面,即 chatTabName.value.chatTabName === undefined
    • 刷新页面,仅当当前 activeId 为 currentId 时

Day12

  • 彻底解决刷新页面后,连接丢失的问题
1
2
3
4
// 监听 activeKey 的变化,更新存储中的值
watch(currentUserId, (value) => {
localStorage.setItem("currentId", value);
});
  • 如上,我将 currentId 存放进 localStorage 中,这样在页面刷新后,可以从 localStorage 中取到当前用户 id,重新发起连接
1
2
3
4
5
// 钩子函数
onMounted(() => {
currentUserId = localStorage.getItem('currentId')
// 主动连接
openSocket(currentUserId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 连接服务器
function openSocket(Id) {
.......................

//指定要连接的服务器地址与端口
const socketUrl = `ws://localhost:8081/api/websocket/${Id}`;
if (socket != null) {
socket.close();
socket = null;
}

// 实例化WebSocket对象,建立连接
socket = new WebSocket(socketUrl);

................................
}
  • 页面刷新之后,选中刚刚选中的 tab 页:
1
2
3
4
5
6
// 监听 activeKey 的变化,更新存储中的值
watch(activeKey, (value) => {
localStorage.setItem("activeKey", value);
});

activeKey.value = localStorage.getItem("activeKey");
  • 回收前面的解决连接丢失的问题,因为直接这行代码就没问题:
1
2
// 主动连接
openSocket(currentUserId);
  • 真奇了怪了,一直都是这样写的,一直有问题,现在倒好,突然没问题了,连接不丢失了
  • 难道是因为我之前多查询了下当前登录用户吗?

1
2
3
getCurrentUser();
// 主动连接
openSocket(currentUserId);

Day13

  • 未登录时,进入聊天大厅获取当前登录用户的 id 时,会报异常

    image-20230921231210339

    • 所以在跳转聊天大厅时,先校验是否登录

    • 如果未登录,则提醒用户先登录,才能享受实时通信服务
    1
    2
    3
    4
    5
    6
    7
    8
    // 前往博客社区
    const goToChat = () => {
    if (!currentUser.value) {
    message.warning("请先登录");
    } else {
    router.push("/chat");
    }
    };
  • 优化用户体验:
    • 未登录时,不能跳转至聊天大厅
    • 未登录时,不能查看个人信息
  • 简单地优化了页面的表现张力:(2023/09/21 晚)

image-20230922003153503

image-20230922003202984

image-20230922003211385

Day14

  • 一篇博客应该有什么信息呢?

  • 完成博客列表优化:

image-20230923165701937

  • 我 HTML + CSS 的功底还是很不错的,基本没遇到什么大问题

现阶段优化指南

  • 博客页面优化:

    • 如何实现每个博文绑定自己的 Like、Collect、Comment,待解决
    • 博文应该带标签,点击对应标签,就可跳转至搜索的博文内容搜索结果
    • 那就是要开发一个博文查找功能,开发博文查找页面,根据输入字段,查询匹配 title、description 的博文
    • 待隔壁 MemorySearch 聚合搜索平台成功落地之后,会在 MemoryChat 博客社区中引入 ES,进行快速查找
    • 开发博客编写页面,用户能够在线编写提交博文
    • 用户收藏夹:多个收藏集,集合中博文的排列列表与搜索博文的结果展示,设想一致(掘金就这么干的)
    • 评论功能,这个应该是核心,不过我肯定一样能轻松搞定
  • 队伍功能优化:

    • 队长可以发布任务(或者说公告),在消息栏中显示
    • 待优化完成用户私聊的所有页面优化、通信体验之后,考虑开发队内聊天
  • 私聊体验优化:

    • 当有聊天消息还未接收时,有消息提醒(可供开启或关闭这个功能)
    • 私聊双方的消息,不够有辨识度
  • 用户中心大厅优化

    • 将来计划把博客社区搞为默认首页,那用户中心是用来干什么的呢?(稍等一下,我问问 AI)
    • 可以展示什么内容呢?介绍下这个网站的内容

Day15

  • 搜索队伍、搜索用户、搜索博客将来统一处理

  • 队伍状态(是否加密,显示) ✔

  • 加密队伍,设置密码 ✔

  • 队伍状态区分,加密队伍有弹窗,提示输入密码
  • 队长可以发布公告
  • 解决了,几天前遗留下来的问题:

    • 其他大厅进入聊天大厅,默认文件传输助手
    • 点击私聊进入聊天大厅,默认选中私聊用户
1
2
3
4
5
6
7
8
// 获取接收者id
chatTabName.value = route.query;
// 记忆选中的Tab标签页
if (chatTabName.value === undefined) {
activeKey.value = localStorage.getItem("activeKey");
} else {
activeKey.value = chatTabName.value.chatTabName;
}
  • 实时双向通信优化:

    • 解决首次进入页面时,聊天消息未渲染的成功问题(这个好像很顽固)
    • 聊天消息要有辨识度

      • 简单地优化了输入框的样式,和表现张力

      • 简单优化了消息的表现形式,目前发现的问题是:消息中没有发送者头像

        • 我们应该在在消息中插入头像字段嘛?这样在表现上会非常方便,但是扩展性太差,还得改动所有已有逻辑

        • 那我们就应该根据 sendId 来查询发送者的信息,再渲染到消息上

        • 只要获取到了发送者的详细信息,就可以更清楚地排列消息,让收发双方的消息更加具有辨识度
    • 不能发送空消息

      • 简单地作了字符串校验,给予了发送成功和发送失败的反馈,在消息发送成功后,清空编辑的消息
  • 优化过后的通信体验(2023/09/24 午)

image-20230924131722543

  • 博客页面优化计划

    • 需要封装一个 DTO 类,返回文章,以及文章作者的所有信息
    • 博客阅读页、博客收藏页、博客搜索页、博客编辑页
  • 博客阅读页的排版:

    • 文章内容、作者信息、文章目录、相关推荐
  • 完成点击不同的博文首页,跳转至不同的展示页面

  • 博文阅读页的优化展示

  • 优化了博客阅读页,能够识别 Markdown 文档:

image-20230924234514376

1
2
3
4
5
6
7
8
9
<div>
<a-list-item-meta
:description="articleInfo.author.email"
>
<template #title>
<a href="https://www.antdv.com/">{{articleInfo.author.username}} </a>
</template>
</a-list-item-meta>
</div>
  • 今日待解决:
    • 私密队伍需输入密码才可申请加入 ✔😈
    • 实时消息没有获取到发送人的详细信息 😈
    • 发布队内公告 ✔😈
    • 首次进入页面时,聊天消息未渲染的成功 ✔😈
    • 博文列表太长了,应该加个每页数量和延时加载效果
    • 识别 Markdown 格式 ✔
    • 博文收藏页、博文搜索页

Day16

  • 先解决昨天的问题

  • 用户新增文章数、获赞数、粉丝数
  • 尝试使用 CSS 实现页面跳转动画,但 Vue 渲染冲突,导致动画重复渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.fade-in-out {
animation: fadeInOut 5s;
}

@keyframes fadeInOut {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
  • 使用 a-affix 组件将 元素固定,这个时候元素的 width 属性不能是百分比了,否则不起作用
  • 固定每周推荐用户、作者、点赞信息

  • 组件撑不开盒子,老有这个问题
    • 妈的,组件存在浮动、定位属性,当然撑不开了
  • 成功解决点赞、收藏功能(每篇文章独有)
  • 解决这个问题的关键就是,为每个文章都分配独有的判定点赞、判定收藏变量:

  • 在获取博文列表时,为每个博文增加这几个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
myAxios
.get("/article/list/VO", {
params: {
currentPage: 1,
pageSize: 5,
},
})
.then((res) => {
if (res.data !== null) {
articleList.value = res.data.records.map((article: any) => {
return {
...article,
isLiked: false,
isCollected: false,
isComment: false,
};
});
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<LikeTwoTone
style="font-size: 18px"
key="like"
:twoToneColor="item.isLiked ? '#ff4d4f' : '#b0c4d8'"
@click="toggleLike(item)"
/>

<HeartTwoTone
style="font-size: 18px"
key="collect"
:twoToneColor="item.isCollected ? '#ff4d4f' : '#b0c4d8'"
@click="toggleCollect(item)"
/>
<MessageTwoTone
style="font-size: 18px"
key="comment"
:twoToneColor="item.isComment ? '#ff4d4f' : '#b0c4d8'"
/>
1
2
3
4
5
6
7
8
// 点赞
const toggleLike = (item) => {
item.isLiked = !item.isLiked;
};
// 收藏
const toggleCollect = (item) => {
item.isCollected = !item.isCollected;
};

image-20230925151826913

Day17

  • 封装消息的内容

    • 可以在后台封装

    • 也可以选择前端发出请求(这个简便)
  • 搞没了,前端拿取每条消息的每个 id 再发送请求,简直做梦,还是搞后端吧

  • 现在把头像换成登录用户的头像

  • 简单地优化了双向通信的消息气泡展示:

    • 标签中定义了动态的 class 属性:(2023/09/26 午)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <div :class="messageClass(item.senderId)">
    <!--发送者-->
    <div style="position: absolute;top: 6px">
    <a-avatar size="large" :src="currentUser.avatarUrl"/>
    </div>
    <div style="margin-left: 24px;font-size: medium">
    <a-list-item>
    <div style="font-size: smaller">
    <!--发送时间-->
    <a-list-item-meta :description="item.sendTime">
    </a-list-item-meta>
    </div>
    <!--消息内容-->
    {{ item.content }}
    </a-list-item>
    </div>
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .senderMsg {
    position: relative;
    left: 1150px;
    }

    .receiverMsg {
    position: relative;
    }

    .receiverMsg {
    position: relative;
    }
    • 动态改变 class
    1
    2
    3
    4
    5
    6
    7
    8
    // 动态变换收发双方消息气泡
    const messageClass = (senderId) => {
    if (currentUserId === senderId) {
    return "senderMsg"; // 如果当前用户的ID等于消息发送者的ID,
    } else {
    return "receiverMsg"; // 其他情况返回空字符串
    }
    };
  • 最终效果:

image-20230926170307814

  • 关于通信双方的详细信息,还得在后端封装,暂时不改了
  • 当文章高度不够全屏时,背景默认为整个屏幕高度;随着内容增多,背景高度会动态增加
1
2
<!--文章信息-->
<a-card style="width: 60%;margin-left: 15%; min-height: 85vh;margin-bottom: 20px">

image-20230926220346416

  • 简单实现了申请加密队伍需要填写密码的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 申请加入队伍
const joinTeam = (teamInfo: any) => {
if (teamInfo.status === 2) {
showModal();
} else {
myAxios
.post("/team/join", teamInfo)
.then((res) => {
message.success("申请入队成功");
})
.catch(() => {
console.log("加入失败");
});
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// 确认
const handleOk = (teamInfo: any) => {
vis.value = false;

myAxios
.post("/team/join", teamInfo)
.then(() => {
message.success("申请入队成功");
})
.catch(() => {
console.log("加入失败");
});
};

image-20230926235757172

  • 完成队伍公告的展示,再实现一个点击查看详细公告吧(2023/09/26 晚)
  • 查看队内公告完成

image-20230926235739393

  • 申请添加好友/进入队伍之前,都应该有弹窗进行反复确认

image-20230927000836068

Day18

  • 简单的评论展示:

image-20230927112918187

  • 简单的博文收藏页:

image-20230927115337147

  • 后端,博文表的字段添加

    • 收藏量(collects)
    • 封面图(articleUrl)
    • 标签(tags)
  • 点击评论图标也可以进入页面
  • 优化下队伍人数的表现、每周推荐用户的匹配度表现
1
<a-progress type="circle" :percent="item.percentage" :width="50" />
  • 这个圆形进度圈的大小,调整起来有点问题
  • 嗨嗨,这个进度条的百分比还让我自己算:

1
2
3
4
5
<a-progress
:percent="((item.joinNum / item.maxNum) * 100).toFixed(2)"
size="small"
status="active"
/>
  • 解决一下默认打开用户中心 -> 我的页面时,没有渲染出已加入的队伍列表
1
2
3
4
5
6
// 默认选中已加入的队伍
const activeKey = ref("1");
// 直接查询已加入的队伍
onMounted(() => {
getJoinedTeam();
});
  • 给队伍表添加队内公告字段
  • 修改公告这个还需要考虑一下:
    • 传入的参数:修改人、修改队伍、公告内容(只有队长才能修改/发布公告)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Boolean updateAnnouncement(AddAnnouncement addAnnouncement, HttpServletRequest request) {
Long userId = addAnnouncement.getUserId();
Long teamId = addAnnouncement.getTeamId();
String announcement = addAnnouncement.getAnnouncement();

if (!getById(teamId).getUserId().equals(userId))
throw new BusinessException(ErrorCode.NO_AUTH, "非队长不能更新公告内容");

UpdateWrapper<Team> tuw = new UpdateWrapper<>();
tuw.eq("id",teamId).set("announcement", announcement);
boolean update = update(tuw);

if (!update)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "更新公告内容失败");

return true;
}
  • 后台接口已开发完毕,前端接口待调通(2023/09/27 晚)

Day19

  • 完善昨晚的发布公告,遇到了些许小问题:

    • 由于编辑公告的弹窗是写在 list 列表中的,即每个 team 都有自己的公告弹窗,所以不应该使用一个变量来控制弹窗的出现/消失:
    1
    2
    3
    4
    5
    6
    // 弹窗
    const visible = ref<boolean>(false);
    // 展示弹窗
    const showModal = (team: any) => {
    visible.value = true;
    };
    • 而应该为每个 team,都分配独有的控制弹窗出现的变量(showEditWindow):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    myAxios
    .get("/team/created", {
    params: {
    loginUserId: userId,
    },
    })
    .then((res) => {
    createdTeamList.value = res.data.records.map((team: any) => {
    return {
    ...team,
    showEditWindow: false,
    };
    });
    });
    • 为每一个 team 绑定:
    1
    2
    3
    4
    5
    6
    7
    <a-button size="large" danger @click="showModal(item)">发布公告</a-button>
    <div style="position: relative">
    <a-modal v-model:visible="item.showEditWindow" title="队伍公告" @ok="updateAnnouncement(item)">
    <p>请编辑队伍公告</p>
    <a-textarea v-model:value="announcement" style="height: 100px"/>
    </a-modal>
    </div>
    1
    2
    3
    4
    // 公告弹窗
    const showModal = (team: any) => {
    team.showEditWindow = true;
    };
  • 这个思路是普适的,在获取后台传回的数组信息时,为数组内每一个元素都封装自己的独有的元素

  • 实现每篇文章的点赞、收藏,以及申请加入队伍中,都是这样的思路(2023/09/28 午)

  • 实现博文列表 加载更多 功能

    • 在评论加载中,也同样适用
    • 放弃了,搞不懂实现原理,日后再看

新增文章评论功能

  • 后端新建评论表:

  • 这个评论表该如何构建呢?表字段:

    • 评论 id、子评论 id、发评者 id、评论内容、评论时间
  • 文章如何绑定评论?
    • 设计一张文章/评论表(太庞大了,每篇文章与其评论的关系都要一一列举)
    • 文章添加字段:评论字段(comments),存储评论 id,当然,仅存取一级评论 id
    • 二级评论 id 当然要从一级评论的字段中找了,子评论字段(comments)存储二级评论 id

Day20

  • 彻底优化实时双向通信的页面、消息气泡表现方式
  • 后端封装 MessageVO 类,在 Message 基础上扩展了两个字段(目前仅需要这两个字段,后续可以做扩展):
1
2
3
4
5
6
7
8
9
/**
* 发送者昵称
*/
private String username;

/**
* 发送者头像
*/
private String avatarUrl;
  • 提供了转换 messageList 为 MessageVOList 的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 转换 messageList 为 MessageVOList
*
* @param messageList messageList
* @return MessageVO
*/
public List<MessageVO> getMessageVOByMessage(List<Message> messageList) {
return messageList.stream().map(message -> {
Long senderId = message.getSenderId();
User user = userService.getById(senderId);

MessageVO messageVO = new MessageVO();
messageVO.setSenderId(message.getSenderId());
messageVO.setReceiverId(message.getReceiverId());
messageVO.setContent(message.getContent());
messageVO.setSendTime(message.getSendTime());
messageVO.setUsername(user.getUsername());
messageVO.setAvatarUrl(user.getAvatarUrl());

return messageVO;
}).collect(Collectors.toList());
}
  • 完全优化了实时双向通信的用户体验:

image-20230930220121786

  • 申请入队:

    • 想实现推动入队消息到队长的消息列表中,队长同意,方可入队
    • 学完消息队列之后再考虑
  • 点赞、收藏博文,

  • 后端接口开发:
    • 点赞/收藏博文,应该传入的参数有:

    • userId、ArticleId

    • 执行使该博文的 likes + 1,并在 article-like 表中存放点赞记录

    • 收藏博文也是如此

    • 慢慢实现吧,最近先把简历优化、算法拔高、项目优化、软考备考做好(2023/09/30 晚)

MemoryChat 开发计划:

  • 使用 Redis 缓存热点消息(最近 36h 内的消息),数据库中存放所有消息
  • 使用 ES 来大幅提升博文检索效率

经验分享

  • 如何实现点击好友私聊后,跳转至聊天窗口的对应聊天窗口?
  • 实现流程:
    • 进入聊天大厅,直接查询我的所有好友信息
    • 聊天大厅直接以tab 标签页的形式,展示了所有好友的聊天窗口,其中,各个tab 页的 key 值为好友的 id
    • 点击私聊,携带好友 id作为参数,从我的好友列表跳转至聊天大厅
    • 跳转至聊天大厅,直接获取传递的 id 参数,并将其值赋给 tab 页的 activeKey
    • tabs 标签页默认选中 key 值为 activeKey 的 tab 页

image-20230916141753064

image-20230916141758222

  • 接下来就是在各个聊天窗口,实现双向实时通信了

网页中支持 Markdown 语法写博客

  • 要在博客网站中支持 Markdown 语法写博客并展示,你可以使用第三方的 Markdown 解析库来解析 Markdown 文本,并将解析后的内容展示在网页上(2023/09/24 晚)
  • 首先,你需要引入一个适用于 Vue 的 Markdown 解析库,例如markdown-it。可以通过 NPM 安装该库:

1
npm install markdown-it
  • 然后,在你的组件中,你可以导入并实例化markdown-it,将 Markdown 文本作为输入,使用.render()方法将其转换为 HTML 并展示在网页上
1
2
3
4
5
6
7
import MarkdownIt from "markdown-it";

// Markdown语法
const parsedContent = ref();
const md = new MarkdownIt();
// 使用Markdown语法接收文章内容
parsedContent.value = md.render(articleInfo.value.content);
1
2
3
4
5
<div
v-html="parsedContent"
style="position: absolute; margin-left: 10px; margin-right: 10px; margin-top: 20px;"
>
</div>
  • 在上面的示例代码中,我们导入了markdown-it

    • 然后在mounted钩子中实例化了MarkdownIt对象
    • 并将 Markdown 文本this.articleInfo.content传递给其.render()方法来解析为 HTML 并赋值给parsedContent
    • 然后我们使用v-html指令将解析后的内容展示在网页上。
  • 这样,无论用户使用 Markdown 语法还是普通的 HTML 编写博客内容,页面都会正确展示

  • 最终效果:

image-20230924235124466

踩坑记录

JSON 转换错误

  • 前台获取服务器转发的消息后,需要解析出接收者,再判断接收者是否为当前用户,是则为该用户展示具体消息
1
2
3
4
5
6
7
8
9
10
11
12
13
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
// 封装返回消息
receiveMsg.value = msg.data;
// 解构
console.log("这条消息是发给: " + receiveMsg.value + " 的");
console.log("这条消息是发给: " + receiveMsg.value.receiverId + " 的");
const content = receiveMsg.value.content;
// 是否属于我的消息
if (receiverId === currentUserId) {
setMessage("服务端回应: " + content + "发给: " + receiverId);
}
};
  • 前端返回的 mes 是 JSON 字符串,我看见控制台输出的内容是,以为是个对象,结果取到的属性值是 undefined:
1
{"senderId":"1657284783190523906","receiverId":"1657284893320364034","content":"阿发","sendTime":"2023-09-14T13:29:11.309Z"}
  • 调试了半天,检查了下 mes 的类型,才发现是个JSON 字符串,奶奶的
1
console.log("type of " + typeof (receiveMsg.value))
  • 所以要将 msg JSON 字符串解析为对象后,才可以正常拿到属性值
1
receiveMsg.value = JSON.parse(msg.data);
  • 但是连接服务器 socket 时,也会执行这里的代码,而服务器首次响应连接请求并传回的的参数类型不是 JSON 字符串

  • 解析失败,报以下错误:

image-20230914214515079

  • 最后优化代码,同时处理两种情况:服务器首次连接成功后的响应(普通字符串) / 服务器转发的消息(JSON 字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
// 封装返回消息
if (typeof msg === String) {
receiveMsg.value = JSON.parse(msg.data);
} else {
receiveMsg.value = msg.data;
}
// 解构
console.log("这条消息是发给: " + receiveMsg.value.receiverId + " 的");
const content = receiveMsg.value.content;
// 是否属于我的消息
if (receiverId === currentUserId) {
setMessage("服务端回应: " + content + "发给: " + receiverId);
}
};
  • 各种坑都能踩到,不过好在花点时间排错,都能解决掉 👏👏👏(2023/09/14 晚)

双向通信调试成功

  • 首先解决上面小坑,那个代码写的还有点问题,前端还得分两次校验是否为 JSON 字符串
  • 于是我把后端服务器首次响应时的响应结果,转换成 JSON 字符串了

image-20230915145545165

  • 又写了好多打印输出代码,好歹完成了核心功能:服务器向指定用户转发消息2023/09/15 午
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
console.log("收到消息!消息为: " + msg.data);
console.log("消息格式为: " + typeof msg.data);
console.log("消息发送者为: " + JSON.parse(msg.data).senderId);
console.log("消息内容为: " + JSON.parse(msg.data).content);
console.log("消息接收者为: " + JSON.parse(msg.data).receiverId);
console.log("您是: " + currentUserId);
if (currentUserId === JSON.parse(msg.data).receiverId) {
console.log("很好,您是这条消息的接收者!");
setMessage(
JSON.parse(msg.data).senderId +
"给您发送了一条消息: " +
JSON.parse(msg.data).content
);
}
};

image-20230915145835165

image-20230915150018962

  • 这里还有好多小坑:(2023/09/15 午)
    • 服务器根据在线人数(即连接的客户端数量),决定转发多少条消息,所以控制台输出了不止一条消息(重复的消息)
    • 在 socket 服务下修改了代码之后,有时需重启浏览器才生效,这种情况还是第一次遇到
    • 当然了,为了测试实时双向通信,我分别开启了Edge 浏览器和 Goole 浏览器进行测试

部署和维护

  • 列出将项目部署到生产环境所需的步骤和配置信息,包括服务器环境要求和数据库设置。
  • 提供项目的维护和支持方式,如 bug 提交、技术支持联系方式等。

TODO

添加好友

  • 用户添加好友上限字段,一个用户应该有好友上限
  • 用户不应该直接添加好友,应该发送好友申请,待对方同意后,二者成为好友关系(如何判断?
1
2
QueryWrapper<Friends> fqw = new QueryWrapper<>();
fqw.eq("user_id", userId).eq("friend_id", friend.getId());
  • 添加成功/失败后的反馈
  • 添加好友后,可支持修改好友昵称

分页查询

  • 后台的分页查询未优化,分页参数写死为 currentPage:1,pageSize:20

异常处理

  • 抛出的异常可以设置独有的错误码,前端妥善解决,优化用户体验
  • 可以考虑封装一个简单的异常处理工具(ThrowUtils)

用户体验

  • 发出添加好友、申请入队请求后,抽屉关闭(?)
  • 用户在线问题需要优化,应该存放进 Redis 中,设置过期时间为 2hour ,现在是直接从数据库中取得
  • 而最大的问题是,用户登录后,修改用户状态为在线,仅用户执行登出操作后,才会修改用户状态为下线
  • 用户进入聊天页面,保持聊天窗口滚动条保持在最底部
  • 私聊时,展示用户昵称
  • 聊天消息目前是设置 2hour 后过期,这其实比较不合理,但是处于测试阶段,测试的通信记录开销还蛮大的,删了挺好
  • 日后再考虑如何优化:定期删除(7 天) + 用户手动清空聊天记录,这个更加合理
  • 聊天页面刷新,会拿不到登录用户的信息,待修复
  • 私聊操作应该更加多元化,可以发送文本消息表情、甚至是收发文件

Redis

  • 考虑封装一个 RedisUtils,统一所有的 Redis 操作

Tab 标签页

  • 遇到了 BUG,tab 标签页的图标显示不出来,还想着在聊天窗口添加好友头像呢

好友私聊

  • 区分聊天双方,可以在聊天窗口内添加用户昵称,也可以实现:双方的消息气泡在窗口的不同侧
  • 收到消息可以弹出消息提示,提示未读消息
  • 刷新聊天大厅,与服务器的连接丢失

Memory 缘忆交友社区-开发文档
https://test.atomgit.net/blog/2023/09/10/Memory 缘忆交友社区-开发文档/
作者
Memory
发布于
2023年9月10日
更新于
2023年9月26日
许可协议