『数据库系统』与爱同航——系统实现文档

系统实现文档

一、实现环境

1.1 客户端环境依赖

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
{
"name": "vue3",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.0",
"element-plus": "^2.3.14",
"mavon-editor": "^2.10.4",
"echarts": "^5.4.2",
"pinia": "^2.1.6",
"undraw-ui": "^0.9.9",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"vite": "^4.4.9"
}
}

UI界面主要使用了Element-Plus,制图主要使用了Echarts,前端使用Vite+Vue3的框架进行开发。

1.2 服务器端环境依赖

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot3</name>
<description>springboot3</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

数据库交互使用Mybatis框架,为锻炼数据库开发能力,所有数据库交互均通过手写SQL完成,而摒弃了目前流行的直接调打包好接口的形式。在Springboot3框架的基础上额外使用了一些实用插件,如Lombok,Hutool等。

1.3 项目技术栈

本项目基于Vite+Vue3,结合Element-Plus,Echarts作为前端技术栈,使用Springboot3集成Mybatis作为后端技术栈,数据库使用了适合大学生宝宝体制的MySql,并在购买的阿里云服务器内Docker环境下安装以便公共访问,同时我们在展示前将使用阿里云服务器结合Nginx进行代码的托管部署。版本管理工具为Gitee,接口测试软件为PostMan,数据库可视化软件为Navicat15,代码编写软件为Idea,数据库兼网站部署的服务器IP地址为:39.107.136.38,Gitee仓库地址为:https://gitee.com/database-assignment_1/database-assignment。

二、项目文件结构

后端文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
src.main: 后端资源父级目录
java: JAVA文件包
com.example:
config: 存放后端配置跨域请求等文件的文件夹
controller: 表现层文件夹,前后端交互的第一层。控制器负责接收请求并将其转发给对应的视图或服务进行处理, 负责处理请求的路由和参数验证
service: 服务层文件夹,前后端交互的第二层。该层负责处理用户请求的核心逻辑,并调用mapper层接口向数据库发起请求。
mapper: 映射层文件夹,前后端交互的第三层,也是直接和数据库打交道的层。该层用于向数据库发送各类请求,并得到返回数据,常配合xml文件共同使用
entity:实体类文件夹,是数据库中的表属性的超集,还可能包含用于返回前端的额外信息
front: 返回前端的打包类文件夹,用于简化后端操作
resources: 其他资源文件包
application.yaml: 后端配置文件,配置数据库,上传文件大小格式等重要信息
mapper: xml映射层文件夹,内含xml文件,处理较为复杂的SQL语句,与JAVA的mapper文件协同使用。

前端文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
vue3: 前端资源父级目录
public:存放一些静态文件,供前端网站使用
src:前端主要文件目录
assets:存放静态文件
components:存放前端使用的组件
icon:存放图标
image:存放图片
router:配置前端路由
stores:存储多文件使用的功能函数
style:存放css文件
utils:配置request请求
views:存放前端所有的模板文件

三、功能架构

功能架构示意图如下:

3.1 灵魂匹配

筛选条件:

  • 自定义,自由多选

  • 以我的标签为条件

  • 清空条件

匹配结果:

  • 包含信息:背景图、头像、昵称、详细信息、标签

  • 申请好友:对中意的匹配对象发送好友申请

  • 对象切换:不满意当前对象,点击切换下一个

3.2 用户中心

用户信息

  • 展示信息:包括用户的背景图、头像、昵称、详细信息、标签

  • 修改信息、上传图片:用户可以对自己的信息进行修改,支持头像、背景图的上传

  • 申请好友/已是好友:用户可以对其他用户进行好友申请

帖子集

  • 展示顺序:按时间排序由新到旧

  • 操作:点赞、查看详情

照片墙

  • 走马灯形式,将用户的所有照片循环播放

3.3 发布帖子/照片

新的帖子

  • 文字内容

  • 图片内容:支持多张图片的上传

  • 查看权限:用户选择是否发布到广场

照片墙更新

  • 原有图片管理

    • 查看大图:查看图片详情
    • 删除图片:从原有照片中删去
  • 新图片添加

3.4 帖子详情

帖子内容

  • 发帖人信息

  • 帖子文字信息

  • 帖子图片信息

对帖子/评论的操作

  • 点赞,点赞数

  • 评论,评论数

评论区

  • 支持多级评论

  • 支持评论之间互相回复

  • 排序:按热度、时间

3.5 消息

顶部导航栏提醒:

  • 实时新消息数:以小红点的形式,提醒用户有新消息

回复我的:

  • 跳转原始内容:点击跳转到回复的对象,支持内容定位,即跳转到网页的对应位置

  • 点赞、评论:在消息页即可对回复信息进行操作

收到的赞:

  • 跳转原始内容:点击跳转到回复的对象,支持内容定位,即跳转到网页的对应位置

  • 跳转个人主页:点击头像,跳转到点赞者的个人主页

系统通知

新的朋友

  • 申请信息:来自其他用户的打招呼内容

  • 是否通过:用户可选择是否通过其他用户成为好友

申请通过

3.6 登陆注册

  • 登录:用户根据用户名和密码来进行登录,同时在登录界面中有提供注册的选项。只有通过登录才可以进入网站的界面

  • 注册:用户在注册页面填写相关的信息,如果填写的信息合法的话,显示注册成功,用户的信息在数据库保存。

  • 兴趣标签选择:在用户注册成功之后,用户进行兴趣标签的选择。兴趣标签是灵魂匹配的部分依据。

3.7 广场

  • 在广场界面中我们可以看到网站中所有公开的帖子。支持只看好友帖子和看所有帖子的分类。同时支持对帖子进行热度排序和时间排序的选项。
  • 对每一个帖子,都可以进行点赞操作和评论操作。点击评论操作可以跳转到具体的帖子界面进行评论。

3.8 聊天

  • 在界面的左侧是好友列表。显示出当前用户的所有好友。同时在好友的姓名旁边显示当前用户和好友的未读信息。

  • 右侧是和好友的聊天界面。显示当前用户和好友的所有聊天消息。用户可以和好友在这里发送消息。

  • 支持好友亲密度显示,显示当前的亲密度数字和亲密度等级。

3.9 后台数据显示

如果当前用户是管理员身份,则可以进入后台管理界面。

3.9.1 主页信息统计

  • 可以查看网站的访问流量统计
  • 可以查看当前网站的每一个界面单独的访问流量

3.9.2 信息管理

3.9.2.1 基本信息管理
  • 用户信息管理:在这里可以查看每个用户的信息,同时可以编辑用户的信息,以及进入其主页查看等

  • 帖子信息管理:在这里可以查看到所有发布的帖子的信息,支持删除和编辑功能

  • 图片信息管理:在这里可以查看到网站发布的所有图片的发送人,发送时间和图片的内容。支持删除功能。

  • 标签信息管理:在这里可以查看到网站设置的个性标签的所有信息。支持修改标签信息和删除特定标签等。实现了标签的动态更新。

  • 匹配交友信息管理:这里主要是针对灵魂匹配的信息统计。这里展示所有的灵魂匹配记录。同时对信息进行处理,统计出灵魂匹配的运行次数,平均成功率,使用次数最多的用户,一次匹配中发送好友申请最多的用户。

  • 用户行为追踪:这里统计用户在网站的行为信息。统计出用户的行为比如点赞回复等操作,以及管理员发布公告等记录。

3.9.2.2 可视化数据展示
  • 用户信息可视化:这里以图表等形式展示出用户注册流量统计,用户男女比例,用户星座比例,用户家乡统计,活跃用户统计的功能。

  • 帖子信息可视化:这里对帖子和评论的信息进行可视化。展示出帖子的热度排行,评论的热度排行。

  • 社交信息可视化:这里统计的是与社交信息有关的消息。统计出用户的最大好友数以及所有用户的平均好友数,以直方图的形式展示。然后是亲密度统计。统计用户和好友的所有亲密度的最高值,以及所有亲密度,用直方图的形式展示。

  • 社交网络图:通过统计每个用户的好友状态,展示出社交网络图,展现当前用户的社交关系。

3.9.3 公告管理

​ 管理员可以对所有的公告进行管理,包括编辑和删除的操作,同时管理员也可以在此界面发布公告。可以选择向特定的用户发布公告。

3.9.4 图片生成(Bonus)

​ 通过对抗生成模型Pix2Pix,用户可以在网站中上传一个建筑物标签图片,模型会生成一张真实世界的图片,目前该功能仅供本地娱乐,后续可用此技术实现用户背景头像的生成与风格迁移。

四、具体功能介绍

4.1 个人主页

image-20231221160221612

信息:

  • 左边:用户信息。从上到下依次为

    • 用户空间背景图

    • 用户头像、昵称

      • 按钮
        • 修改信息:若该主页是当前登录用户的个人页面,显示修改信息按钮
        • 申请/已为好友:若该主页和当前用户不同,两个用户未成为好友时显示申请好友按钮,否则显示已为好友按钮
  • 用户详细信息:如性别、个性签名、籍贯、学号、真实姓名、学院、年级等

  • 用户标签:用户的个人标签,反应该用户的性格、爱好、经历等等。

  • 右边:用户发布的所有帖子/照片

    • 帖子列表:按时间排序由新到旧,显示该用户发表过的所有帖子。每个帖子包括信息

      • 发帖信息:发帖人的头像、昵称,发帖时间
      • 帖子的文字内容
      • 帖子的图片内容:每三个图片一行
      • 帖子点赞数、评论数
    • 照片墙:以走马灯形式展示,所有图片会自动循环展示。

    • 帖子集和照片墙切换按钮

功能:

修改个人信息:

  • 默认输入为原有个人信息

  • 学院支持联想输入,即输入学院名称的前几个字自动筛选出对应学院,方便快捷。

  • 年级实现多选功能

  • 头像和背景图可以点击替换,并在当前页面预览。

  • 点击下一页/上一页换页,更改不同的信息。

  • 个人标签可以多选,默认为原有标签

  • 点击提交修改可以成功修改个人信息,点击取消或对话框外的任意位置可以关闭对话框。

申请好友:

对于非好友用户,点击添加好友,在弹出的输入框中输入内容,可以向该用户发送一条申请信息;好友用户则显示为已是好友,不能再发送申请信息。

1.3.21.3.1

1.3.3

点赞帖子:

​ 点击点赞,会实时改变点赞状态和点赞数。若当前用户对该帖子点赞过,则显示为红色点赞图标,再次点击会取消点赞,点赞数减一;否则显示为白色点赞图标,点击后变红,点赞数加一。

查看帖子详情:

​ 点击评论按钮或者帖子图片(如果该帖子有图片内容的话),跳转到帖子详情页。

帖子集和照片墙切换按钮:

​ 点击切换展示内容,默认为帖子集。

1.6

切换照片:

​ 可通过左右按钮或下方的进度条切换查看照片。

4.2 发布帖子/照片

信息:

  • 新的帖子:从上到下依次为
    • 发表按钮
    • 文字内容编辑区
    • 图片内容编辑区
    • 查看权限设置区
  • 照片墙更新:从上到下依次为
    • 发表按钮
    • 原有图片管理区
    • 新图片添加区

功能:

功能切换:

点击新的帖子/照片墙更新实现功能切换

2.2

原有照片管理:

鼠标悬停在原有照片上,会出现 放大删除 两个图标,点击触发对应功能。

2.3

添加新的图片:

点击下方的 + 号,选择图片文件上传。

2.4

新的帖子:

图片操作类似上述,图片右上角为绿色对钩说明上传成功;文字编辑区默认高度为3行,高度随输入变化,也可以手动调节高度(在红框处);最下方可设置查看权限;右上角可发表帖子/发布修改后的照片墙。

2.5

4.3 消息

导航栏最右:

​ 以红点形式显示实时消息数目,超过100条消息会显示为99+;若无新消息则不显示红点。

消息页面:

A. 左侧为消息中心导航

  • 分为5大部分,对应不同的消息类型;
  • 若有新消息,会显示新消息对应条数,用户查看该模块时仍然保持,便于用户了解新消息条数,直到离开该模块才会消失。

B. 右侧为具体消息内容及操作:

  • 回复我的:回复当前用户发布过的帖子/评论的消息。包括以下内容

    • 回复者的信息:头像、昵称
    • 回复的对象:reply:xxx,即当前用户发布过的帖子/评论
    • 回复的时间

  • 收到的赞:点赞当前用户发布过的帖子/评论的消息。包括以下内容

    • 点赞者的信息:头像、昵称
    • 点赞的对象:like:xxx,即当前用户发布过的帖子/评论

    3.2

  • 系统通知:由系统发布的消息。包括消息时间和消息内容

  • 新的朋友:其他用户发来的交友申请

    • 申请者信息:头像、昵称
    • 打招呼内容
    • 时间
    • 是否已添加该用户为好友

    3.4

  • 申请通过:当前用户向其他用户发过的好友申请,若被其他用户通过,当前用户会收到申请通过提醒。

功能:

通用功能:

  • 点击用户头像跳转到该用户的个人主页
  • 对单条消息的删除
  • 对某一类型的所有消息清空

回复我的:

  • 点击回复内容 reply:xxx,可以跳转到对应位置。

    • 若源内容为一个帖子,跳转到帖子的详情页。

    • 若源内容为一个评论,跳转到回复所在的帖子详情页后,滑动到回复所在的位置。

  • 对每条回复,可以进行点赞和评论,点赞效果实时显示。

    3.7

收到的赞:

  • 点击源内容 like:xxx,可以跳转到对应位置。

新的朋友:

  • 若还未成为好友,点击 通过 按钮即可添加为好友

  • 若已成为好友,显示为 已通过 按钮,不再进行操作。

4.4 帖子详情

4.2

信息:

  • 发帖者信息:头像、昵称
  • 帖子信息:发帖时间、帖子的浏览量、点赞数、评论数
  • 主评论框:显示当前用户的头像、输入评论的内容
  • 评论区:包括一级评论和多级评论
    • 一级评论:直接对帖子进行评论
    • 多级评论:
      • 显示:回复一级评论或回复一级评论下的其他评论,都显示在该一级评论下
      • 被回复人:若直接回复一级评论,不显示被回复人;否则,在评论内容的最前面显示 @xxx,其中xxx为被回复人的昵称
    • 评论的互动信息:点赞数、评论数 (只在一级评论显示)

功能:

点赞 帖子/评论

点击对应位置的点赞图标,会实时改变点赞状态和点赞数。若当前用户对该 帖子/评论 点赞过,则显示为红色点赞图标,再次点击会取消点赞,点赞数减一;否则显示为白色点赞图标,点击后变红,点赞数加一。

评论 帖子/评论:

  • 帖子:

    • 点击主评论框,评论框会从灰色变成蓝色,聚焦于此,显示发表评论按钮,即可输入评论内容;
    • 点击发表评论即可发布;
    • 点击主评论框外任意位置,评论框会从蓝色变成灰色,取消聚焦,隐藏发表评论按钮
  • 评论:点击任意评论的 评论图标,即可进行评论;再次点击 评论图标,或点击其他评论的 评论图标,即可关闭当前评论框。

    • 4.4.2

评论排序:

评论区的一级评论默认按照热度排序,点击右上角按钮,可以切换 按热度排序 或者 按时间排序。

4.5.2

4.5 灵魂匹配

信息:

  • 选择标签页:用户可以选择自己感兴趣的标签,灵魂匹配会根据选择的标签来返回匹配列表。

    • 多个标签种类,每个种类下有多个标签,用户可自由选择
  • 匹配结果页:本次灵魂匹配的结果。

    • 左侧:对方用户的信息栏
    • 右侧:对方用户的照片墙

功能:

选择标签页:

  • 多选:通过滑动纵向滚动条和横向滚动条,用户可自由多选标签。
  • 我的:点击右上方 我的标签 按钮,可以选中所有当前用户的个性标签,以实现同类匹配。
  • 清空:点击右上方 清空选择 按钮,可以清空当前已选标签,以重新进行选择。
  • 匹配人数:点击右下角的 - / + 按钮,或直接在输入框中输入数字,可以选择匹配人数。若数字不合法,则不会进入匹配。
  • 开始匹配:点击开始灵魂匹配,进入匹配结果页。

匹配结果页:

  • 添加好友:匹配结果保证匹配到的对象都不是当前用户的好友。点击 添加好友 按钮,编辑申请信息后,即可发送好友申请。
  • 切换照片:以走马灯形式展示,匹配对象的图片会自动循环展示。可通过图片左右小按钮或下方的进度条切换查看照片。

  • 切换对象:点击页面左右大按钮,可切换当前匹配对象;一次匹配结果的匹配对象循环显示。

4.6 用户管理

4.6.1 登陆界面

登录的界面如下:

154

前端调用login函数完成登录的整体逻辑操作。将当前用户填写的信息返回到后端,如果后端返回登陆成功后则将界面设置到主页。登录界面中可以点击“注册”进入到注册页面.

4.6.2 注册界面

注册界面如下:

158

前两页是用户的注册页面。用户输入表单的信息后, 前端将信息发送到后端,通过后端的返回值和前端的检查判断是否注册成功,如果注册成功就跳转到标签选择界面,如果注册失败就展示对应的弹窗然后跳转到第一页。

4.6.3 兴趣标签选择

进入到标签选择页面后,用户根据自身的兴趣和特性选取自己的标签。

4.7 广场

广场的主要功能是展示网站中所有公开的帖子,便于用户进行查看和交互。

广场的界面如下:

160

161

广场界面获取当前所有公开的帖子并且展示。同时支持选择功能和筛选功能。选择功能是只看好友和看所有帖子。筛选功能是将帖子按照时间来排序,将贴子按照热度来排序。这里热度的计算逻辑是:HOT = 点赞量 x 5 + 评论数 x 10。下图为排序的位置。

4.8 聊天

聊天界面主要用于用户好友列表的展示以及支持用户和好友的聊天功能。界面如下:

左侧是当前用户的好友列表,右侧是用户与选定好友的聊天界面。右面的聊天功能:聊天功能的逻辑是用户点击发送后,后端接收到message并且存储到数据库中,然后前端按照相对较短的频率反复获取数据库中的message,实时的展示两个人之间的聊天功能。下图展示聊天的过程。

可以发现消息成功发送。

未读消息:我们为好友聊天加入了未读信息的逻辑。当当前用户没有看过好友发的消息时,界面将显示。当用户点击相应的好友时,未读消息的界面将消失。

167

亲密度展示:我们针对好友之间的交流情况,给定了好友之间亲密度的概念。好友之间亲密度的公式如下: 私聊每条+2,点赞对方的帖子 +5,点赞对方的评论 +3,评论对方的帖子 +10,评论对方的评论 +7。由此我们在chat界面展示与好友的亲密度。用不同的颜色代表不同的亲密度等级。

具体亲密度等级和颜色的对应关系为

亲密度(curIntimacy) image
curIntimacy < 100 💛
100 <= curIntimacy < 200 💙
200 <= curIntimacy < 300 💜
300 <= curIntimacy < 400 🧡
400 <= curIntimacy < 500
500 <= curIntimacy < 600 💗
600 <= curIntimacy < 700 💘
curIntimacy >= 700 💕

curIntimacy作为friend的属性被同步更新,在每次点击好友时更新。

4.9 后端数据显示

数据展示区域是作为管理员权限,用于数据展示,数据管理,发布公告等功能的后台平台。

4.9.1 网站数据展示

主要是展示网站流量等相关内容。具体界面如下:

170

界面记录了网站访问流量统计和每个网页的访问流量信息统计。

4.9.2 信息管理

用户信息管理

该界面展示网站所有用户的信息。同时支持用户的新增,删除,编辑等操作。其中编辑操作的界面如图:

同时支持用户信息的导入导出功能。可支持导入导出Excel文件的操作。导出的Excel的部分截图如下:

帖子信息管理

主要进行帖子的管理等操作。页面如下:

176

帖子支持批量删除和导出功能。导出的excel文件如下:

同时帖子支持搜索功能。可以根据帖子的内容或者帖子的作者来筛选出相对应的帖子。下面是示例:

支持单个参数和多个参数进行搜索。查看详情按钮会跳转进对应的帖子动态路由。

图片信息管理

图片管理主要是展示网站中用户发的所有图片,可以对图片进行操作。界面如下:

180

同样的,界面支持图片的删除以及搜索功能。搜索示例如下:

!

标签信息管理

标签信息管理的功能是提供对当前兴趣标签的增删查改。界面如下:

184

​ 同样提供标签的搜索功能,新增标签,删除标签,修改标签,导入导出等功能。

匹配交友信息管理

这里主要是展示灵魂匹配界面所实现的功能。

可以看到,这里记录了网站所有使用灵魂匹配的记录,以及关于灵魂匹配的一些统计数据。

用户行为追踪

这里主要是记录用户的行为记录,比如点赞,评论等行为,以及管理员的行为等。同样支持搜索功能。

行为类型分为:like,reply,system,success,friend

4.9.3 可视化数据展示

这里主要分为用户信息可视化,帖子信息可视化,社交信息可视化。

用户信息可视化

我们分别对用户的注册流量,用户的男女比例,用户的星座比例,用户的家乡地,以及活跃用户流量做出可视化统计,通过可视化用户数据我们可以更好地挖掘数据特征,同时分析网站的实时运营情况。

帖子信息可视化

我们分别就帖子热度最高前20名与评论热度最高的前20名进行统计排序,并展示出实时热度与对应路由,便于管理员快速查看,可以将其理解为一种“热搜榜单”。

189

社交信息可视化

我们就每个人的好友数与好友间的亲密度绘制直方图,直观的查看用户的交友行为,同时我们支持自由调节最小值与直方图间距,进行不同粒度的统计。在图表最上方会展示出一些统计量,比如亲密度最高的两人,我们会实时追踪这些最值,如若最后二人因本网站结缘并牵手成功,这将是我们的荣幸。

同样我们就用户间联系绘制社交网络图,支持鼠标缩放与自由拖动,节点处展示用户姓名,可以更好的查看 好友间联系甚至是一些具有完全图属性的“小团体”,又或是一个好友都没有的”孤家寡人“

4.9.4 发布管理员公告

管理员可以在这里选择成员发布管理员公告,同样可以看到当前发布的所有公告,支持删除以及导出等功能。

同时管理员可以在这里发布公告,支持选择指定人发送或全选所有人发送,在考虑用户人数增多时产生的难以寻找指定目标问题,我们对此也增加了用户名检索功能。

img.png

195

发布后可以实时看到公告,支持按照通知时间和接收人数排序。

4.9.5 背景生成

我们使用pix2pix对抗网络生成模型,在云服务器上进行部署。

e0737f24fa387b22d02e4b5872b8092

不仅增加了趣味性,也为未来的用户生成自己的背景等场景提供了一种可行的方式。

五、部分源代码说明

5.1 聊天功能代码实现

那么首先是获取好友列表的代码逻辑。

前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function getFriends() {
return new Promise((resolve, reject) => {
request.get("/chat/friend", {params: {uid: Id}}).then(res => {
if (res.status === 200) {
friendList.splice(0); // 清空 friendList,确保它是响应式的
for (const friend of res.data) {
friendList.push(friend); // 逐个添加好友到 friendList
}
resolve();
} else {
ElMessage.error({
message: "好友列表导入失败",
showClose: false
});
reject();
}
});
});
}

后端代码:

1
2
3
4
5
public List<Map<String, Object>> getFriendsInfo(Integer uid) {
return chatMapper.getFriendsInfo(uid);
}
//sql代码
@Select("select u.id, u.nickname, u.avatarUrl, f.intimacy from user u inner join friend f on f.userId1 = u.id and f.userId2 = #{uid} or f.userId2 = u.id and f.userId1 = #{uid}")

然后是右面的聊天功能:聊天功能的逻辑是用户点击发送后,后端接收到message并且存储到数据库中,然后前端按照相对较短的频率反复获取数据库中的message,实时的展示两个人之间的聊天功能。

具体实现代码如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
async function Initial() {
await getFriends(); // 使用 await 等待异步操作完成
curFriendId = friendList[0].id;
//curNickName = friendList[0].nickname;
await getRecords();
await request.post("/chat/readAll?senderId=" + Id + "&receiverId=" + curFriendId)
.then(res => {
if (res.status === 200) {
// 处理响应
}
});
await getUnReadMessage();
}
onMounted(async () => {
await Initial(); // 使用 await 等待 Initial() 函数执行完毕
setInterval(getRecords, 500);
await nextTick()
scrollToBottom()
});
//获取聊天记录的逻辑
async function getRecords() {
const prevFriendList = recordList.value
fresh.value++;
fresh.value = 0;
if (isClick) {
await request.get("/chat/getChatRecord", {params: {uid: Id, friendId: curFriendId}}).then(res => {
if (res.status === 200) {
records = res.data;
recordList.value = res.data["chats"];
// console.log(recordList)
if (recordList.value.length !== prevFriendList.length) {
setTimeout(() => {
scrollToBottom();
}, 500);
}
} else {
ElMessage.error({
message: "好友列表导入失败",
showClose: false
})
}
})
if (flag.value === 1) {
// await nextTick()
// document.body.offsetHeight;
setTimeout(() => {
scrollToBottom();
}, 500);
flag.value = 0
}
}
}

5.2 广场的选择和筛选帖子功能

筛选功能是将帖子按照时间来排序,将贴子按照热度来排序。这是两种排序方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sortByTime() {
console.log(onBoardList.value);
onBoardList.value.sort((a, b) => {
const timeA = new Date(a.time);
const timeB = new Date(b.time);
return timeB - timeA;
});
fresh1.value--;
}
function sortByHot() {
onBoardList.value.sort((a, b) => {
let value1 = a.clickNum * 5 + a.commentNum * 10;//算出入读
let value2 = b.clickNum * 5 + b.commentNum * 10;
return value2 - value1;
});
fresh1.value--;//fresh1用于模板实时响应
}

然后是选择功能。选择功能是只看好友和看所有帖子。代码逻辑如下:

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
async function changeOnlyFriend() {
await getFriends();
onBoardList.value = [];
//console.log(allPosts.value);
//console.log(friendList);
for(let post of allPosts.value) {
if(isFriend(post.userId)) {
onBoardList.value.push(post);
}
}
fresh1.value++;
}
async function getFriends() {
return new Promise((resolve, reject) => {
request.get("/chat/friend", { params: { uid: userId } }).then(res => {
if (res.status === 200) {
friendList.splice(0); // 清空 friendList,确保它是响应式的
for (const friend of res.data) {
friendList.push(friend); // 逐个添加好友到 friendList
}
resolve();
} else {
ElMessage.error({
message: "好友列表导入失败",
showClose: false
});
reject();
}
});
});
}

六、系统重要功能实现

6.1 鉴权实现

6.1.1 本地保存登录信息

为了鉴别用户身份,我们采用本地保存 userInfo 作为用户的唯一标识。用户登录后,服务器校验用户身份并返回用户信息;客户端中储存该信息,并在相应的请求中在请求头中附带 userInfo 中的部分信息,以示身份;最终,服务器根据用户身份进而返回数据。
除此之外,userInfo 还可以作为标识用户是否登录的载体,因为只有登录过的用户,才会记录不为空的 userInfo。

1
2
3
4
5
6
7
8
9
10
11
//XXX.vue
//localStorage保存当前登录信息
const userInfo = res.data;
localStorage.setItem('user', JSON.stringify(userInfo));

//main.js
//全局保存的用户信息
const user = JSON.parse(localStorage.getItem('user')) || null;
if (user) {
app.config.globalProperties.$user = user;
}

6.1.2 路由守护

为了防止用户未登录直接通过网址访问个人中心页等等,我们设置网页的全局路由守卫。此处的逻辑采用前置守卫,即在进入每个页面前检测用户是否登录,从而显示给用户正确的内容。

对于需要用户登录页面,只需在 route 路由文件中设置其 meta 路由元信息,将其中 requireLogin 属性置为 true。触发前置路由时,会首先检测当前页面 requireLogin 属性是否为true,如果为 true,则将未登录用户路由到登录界面,以提醒用户需要先登录才能访问当前页面。

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
//main.js
// 添加导航守卫
router.beforeEach((to, from, next) => {
const isLogin = user!=null/* 根据您的实际情况获取用户登录状态 */;
console.log("isLogin" + isLogin);
if (to.matched.some(record => record.meta.requireLogin)) {
// 如果路由需要身份验证,并且用户未登录,则重定向到登录页面
if (isLogin) {
next();
} else {
next('/login');
}
} else {
// 如果路由不需要身份验证,则继续正常导航
next();
}
});

//rounter.js
{
path: '/chat',
name: "chat",
component: () => import('../views/match/ChooseTag.vue'),
meta: {title: "选择标签", requireLogin: true},
}

6.2 推荐算法

我们提出了带权重的杰卡德相似度算法,可以更好的根据每个标签权重的不同进行灵魂匹配,我们认为存在着一些与众不同的独特标签,可以是小众爱好,又或者是宗教信仰,一旦二人匹配到此标签,则应该赋予更大的权重。我们默认起始标签权重皆为1,后续管理员可进行更改。

具体的代码实现如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public Pair<List<User>, Integer> match(Integer uid, List<Integer> labelIds, Integer num) {
User user = userMapper.getUserById(uid).get(0);
String targetSex = null;
switch (user.getSex()) {
case "男": {
targetSex = "女";
break;
}
case "女": {
targetSex = "男";
break;
}
default:{
System.out.println("兄弟你什么性别");
targetSex = "男";
break;
}
}
// 得到候选者
List<Integer> friends = friendMapper.getUserAllFriendId(uid);
List<User> targetUsers = userMapper.getUsersBySex(friends, uid, targetSex);
List<Label> labels = labelMapper.getLabels(); // 得到所有标签
HashMap<Integer, Float> label2weight = new HashMap<>();
for (Label label : labels) {
label2weight.put(label.getId(), label.getWeight());
}
Set<Integer> set1 = new HashSet<>(labelIds);

// 为候选者打分
for (User targetUser : targetUsers) {
List<Integer> targetLabelIds = labelMapper.getUserLabelList(targetUser.getId());
// 度量两个集合的相似度
float divide = 0, divided = 0;
// 受杰卡德相似系数的启发(交集 / 并集),我们提出了自己的相似度计算方法,我们(管理员)为每个标签赋予权重(0~1),在此基础上计算(并集 * 各项权重 / 交集 * 各项权重)
Set<Integer> set2 = new HashSet<>(targetLabelIds);
Set<Integer> combine = new HashSet<>(); // 并集
combine.addAll(set2);
combine.addAll(set1);
Set<Integer> intersection = new HashSet<>(); // 交集
for (Integer integer : set1) {
if (set2.contains(integer)) {
intersection.add(integer);
}
}

for (Integer i : intersection) {
divided += label2weight.get(i);
}

for (Integer i : combine) {
divide += label2weight.get(i);
}
if (divide != 0) {
targetUser.setScore(divided / divide);
} else {
targetUser.setScore(0);
}
}

// 降序排序
Collections.sort(targetUsers);

num = targetUsers.size() > num ? num : targetUsers.size();
targetUsers = targetUsers.subList(0, num);

// 获得照片集
for (int i = 0; i < num; i++) {
targetUsers.get(i).setImageBoardUrls(imageBoardMapper.getImageBoardUrlsByUid(targetUsers.get(i).getId()));
}
matchMapper.addSoulMatch(uid, num);
Integer matchId = matchMapper.getCurMatchId();
return Pair.of(targetUsers, matchId);
}

6.3 文件上传

我们的文件上传方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/upload")
public RestBean upload(@RequestParam MultipartFile file) throws IOException {
// 设置保存路径
String fileUploadPath = "vue3\\public\\image\\";
// MultipartFile 多媒体文件
String originalFileName = file.getOriginalFilename();
String type = FileUtil.extName(originalFileName);
// 为文件重命名
String uuid = IdUtil.fastSimpleUUID();
String fileUUID = uuid + "." + type;
File uploadFile = new File(fileUploadPath + fileUUID);
//先上传文件到磁盘
FileUtils.copyInputStreamToFile(file.getInputStream(), uploadFile);
String url = "/public/image/" + fileUUID;
// 返回给前端 on-success 方法中
return RestBean.success(url);
}

这段代码实现了文件上传的功能。它接收上传的文件,将文件保存到指定路径下,并返回文件的URL给前端。 目前由于需求原因,我们的网站只使用了图片上传,但是通过这个方法可以实现文件上传以及视频上传的功能。

6.4 动态路由匹配

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,在个人主页页面,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:

1
2
3
4
5
6
{
path: '/personal&uid:userId',
name: "PersonalPage",
component: () => import('../views/user/PersonalPage.vue'),
meta: {title: "个人主页", requireLogin: true},
}

这样,访问id为1和id为5的用户主页,只需访问 /personal&uid1/personal&uid5 即可。

需要注意的是,同一路由的不同参数不会引起界面的刷新,导致虽然切换到了 /personal&uid5 ,界面仍然显示 id 为1的用户信息。我们采用监视参数变化的方式,及时刷新页面。

1
2
3
4
5
6
7
8
9
const routeParams = ref(router.currentRoute.value.params);
watch(() => {
routeParams.value = router.currentRoute.value.params;
// 在这里可以处理参数变化时的逻辑
if(routeParams.value.userId !== userId) {
// 例如,你可以在这里触发重新加载数据的操作
location.reload()
}
}, { deep: true });

对于具体的 /personal&uid1/personal&uid5 都可以匹配到这个路由, 对应的值都会设置到 $route.params 中,可以直接取出来使用,从而加载对应的用户信息:

1
const userId = router.currentRoute.value.params.userId;

6.5.触发器

为简化开发者逻辑,并充分利用数据库系统,我们使用了触发器和函数等功能,进一步优化数据库设计。

在给帖子点赞/撤销赞时更新帖子的点赞量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TRIGGER addPostLikeNum
AFTER INSERT ON like2post
FOR EACH ROW
BEGIN
SET @likenum := (SELECT likeNum from post WHERE id = new.postId);
UPDATE post SET likeNum = @likenum + 1 WHERE i
CREATE TRIGGER delPostLikeNum
BEFORE DELETE ON like2post
FOR EACH ROW
BEGIN
SET @likenum := (SELECT likeNum from post WHERE id = old.postId);
UPDATE post SET likeNum = @likenum - 1 WHERE id = old.postId;
ENDd = new.postId;
END

在给评论点赞/撤销赞时更新评论的点赞量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TRIGGER addCommentLikeNum
AFTER INSERT ON like2comment
FOR EACH ROW
BEGIN
SET @likenum := (SELECT likeNum from `comment` WHERE id = new.commentId);
UPDATE `comment` SET likeNum = @likenum + 1 WHERE id = new.commentId;
END
CREATE TRIGGER delCommentLikeNum
BEFORE DELETE ON like2comment
FOR EACH ROW
BEGIN
SET @likenum := (SELECT likeNum from `comment` WHERE id = old.commentId);
UPDATE `comment` SET likeNum = @likenum - 1 WHERE id = old.commentId;
END

帖子下发表/删除评论更新帖子的评论量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TRIGGER addCommentNum
AFTER INSERT ON `comment`
FOR EACH ROW
BEGIN
SET @commentnum := (SELECT commentNum from post WHERE id = new.replyPostId);
IF new.replyCommentId IS NULL THEN
UPDATE post SET commentNum = @commentnum + 1 WHERE id = new.replyPostId;
END IF;
END
CREATE TRIGGER delCommentNum
BEFORE DELETE ON `comment`
FOR EACH ROW
BEGIN
SET @commentnum := (SELECT commentNum from post WHERE id = old.replyPostId);
IF old.replyCommentId IS NULL THEN
UPDATE post SET commentNum = @commentnum - 1 WHERE id = old.replyPostId;
END IF;
END

评论下增加评论时更新评论的评论量:

1
2
3
4
5
6
7
8
9
10
11
CREATE TRIGGER addCommentNum2Comment
AFTER INSERT ON `comment`
FOR EACH ROW
BEGIN
SET @commentnum := (SELECT commentNum from `comment` WHERE new.replyCommentId is
not null and id = new.replyCommentId);
IF @commentnum IS NOT NULL THEN
UPDATE `comment` SET commentNum = @commentnum + 1 WHERE id =
new.replyCommentId;
END IF;
END

七、总结

YuKi

与爱同航从第四周开始起步,一直到十六周堂堂完结,这一路上有太多的心血,太多的收获和感动。在这将近三个月的时间中,与爱同航从立项开始到如今已经长成了参天大树,而我们三个也在开发网站的过程中收获了知识。

那么与爱同航是怎么诞生的呢?我还记得是一天晚上闲逛,杨博文跟我说,要不咱数据库做一个恋爱交友网站,解决一下我的个人问题,我一想这挺好啊,既做了作业又有了女朋友,这不是一举两得吗。于是欣然应允。所以与爱同航是带着大家的期望和一点小小的私心诞生的。

于是每周约研讨室的日子开始了。让我印象比较深的是第一次开会大家分析需求和设计表。大家调研了多种恋爱软件,列出我们的需求,并且根据数据库表的格式进行优化,并且最终生成了设计文档。而我觉得这是我们开发过程目标比较明确的原因。万事开头难,一定要从开头就设计好。

我们的核心机制是灵魂匹配功能,大家为了这个功能也做了很多的调研,最终决定了根据兴趣标签,使用带权重的杰卡德相似度算法来进行推荐,做出了很好的效果。

与爱同航是我们我们三个人第一次一起开发的网站,我们都对此投入了足够的热诚,我相信,在我们的心里与爱同航已经不是作为一个作业存在了,而是个人生涯中的一个里程碑和记忆中浓墨重彩的一笔。

最后非常感谢刘瑞老师的教导和助教的帮助。以一首诗作结吧:新故相推书画卷,丹青妙手向翠峰。希望与爱同航帮助更多的人获得幸福!

Yang

从9.12号建群,到12.24号我在进行最后的系统文档审查,这一路走来的确经历了太多,我们应当算启动的较早的组,在第四周很多人没组好队时我们就已经勾搭上了,然而还是开发到了ddl前的最后一天。

开发过程中,由于和开发组的另外二位都是一起并肩作战过的老熟人了,因此彼此交流比较顺畅,想法思路也比较一致,我们花的最多的时间是在前期准备上,从为整活提出恋爱交友网站开始,到认真起来去分析功能,设计数据库,再到进行开发时间规划安排,将功能和数据库整理成文档,再到购买服务器配置开发环境,编写接口文档,这一切的一切已经接近于实际的开发流程,事实同样证明,好的准备是成功的一半,在后续的开发过程中,除了大家第一次写Vue代码时遇到的细小问题,几乎没有出现过其他问题。在提交时我也会一同附上所有文档。

本网站的功能类似大多数交友网站,在其基础上增加了自己的创新,无论如何,本网站共计JAVA(3800)+CSS(600)+JS(500)+Vue(9200)+Xml(300)约1万5千行源码,加上各类设计报告与文档,Gitee上共提交200余次,主楼研讨室每月每人都会约满,可以说它占据了我们大三上的大部分时间(和编译五五开吧),投入了彼此最宝贵的心血,诚然该网站还有许多值得优化之处,但对于一份数据库大作业而言,我对最后成果满意到无以复加。

要说经历此次开发我收获最大的是什么,那便是团队合作中交流的重要性,以前在六系都是一个人(和jkm)单打独斗,借由数据库课程第一次亲身参与了团队开发,也理解了计算机人不只是需要Coding能力,交流能力也同样关键。最后感谢苗姐与宇骏的辛苦付出,旅途总有终点,希望我们还有下次合作的机会,respect了家人们。

在后续我们会将网站部署到公网,希望(也许)能够帮助大家找到真爱。

MorningTxT

从9.24在gitee上成立仓库,到10.29开始正式的登录注册页面编写,再到12.23微调结束、彻底收尾,数据库大作业也像过山车一样,进度忽快忽慢,方向忽上忽下,然而柳暗花明,过山车也终于平稳落地。

以评论区的开发为例。虽然在最初就已经确定了需要实现二级评论功能,但真正实践起来,却是一波三折。第一次我先是自己手搓了一个丑丑的评论区,但因为没有参考,各大网站的实现方法八仙过海,令我在排版布局和实际功能实现的时候也顾左顾右,进度缓慢。某一天突发奇想,直接在百度上搜索vue3中的评论区实现,找到了一个名为 Undraw UI 的组件库,其中完美地实现了评论区的丰富功能,令人大喜过望,我们立即将其投入到自己的网站中使用。然而好景不长,先是发现由于该组件集成度太高,数据的输入输出高度规范化,列表套列表的复杂结构给后端同学带来了不小的压力;中间又发现了该组件的 bug ,在QQ群中给开发者提供反馈;后是在消息功能的实现中,发现该组件无法获得 回复/点赞 对应的原评论,而这是实现消息功能的基石。于是开始第三遍的评论区重写,以 Undraw UI 的排版布局为参考,实现了点击聚焦评论框、点击弹出评论框、回复对象昵称高亮、最新/最热排序等特色功能。在此之后,还经历了评论换行符的错误显示等小小风波。不过努力终有回报,在消息界面跳转到原内容的实现上,通过给每条评论增加锚点的方式,成功实现了对应位置的跳转滑动,成果喜人。

相较于其他组的手忙脚乱,我很高兴能和两位有热情、负责任、有能力的优秀同学一起合作,齐心合作、深入讨论、互相勉励,将学期初的宏伟蓝图落地为美观使用的具体项目,经历了一次愉快的团队开发。

在此,由衷地感谢组内每一个成员,没有他们的辛苦付出,也没有我们今天的成功;也感谢老师和助教的大力支持,我们希望能得到你们的认可,为本课程画上一个完美的句号。