软件工程第二次结对作业
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/SE2024 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/SE2024/homework/13281 |
这个作业的目标 | 1.学习基础的前后端WEB开发 2.制作校园项目平台web应用程序 |
学号 | 102201130 |
结对成员学号 | 102201125 |
结对成员博客链接 | https://www.cnblogs.com/huos1/p/18457195 |
git项目链接 | https://github.com/gsgqh/pg |
一、项目分工
郑哲浩
- 后端开发:实现项目的后端接口逻辑,负责项目后端数据库的处理逻辑
- 接口开发:负责前后端交互的开发,使得前端的接口与后端的接口对接,使得前端根据后端的API请求或者返回数据,以及前端一部分的交互逻辑
- 测试: 负责后端测试
林智烽
- 前端开发:负责实现项目的前端逻辑,包括UI组件等的交互逻辑
- 前端样式:负责实现前端的美术样式,包括动态效果,背景效果等等
- 测试: 负责前端测试
二、PSP表格
项目名称:PG
任务名称 | 预估耗时 (h) | 实际耗时 (h) |
---|---|---|
系统设计 | 2 | 3 |
数据库设计 | 3 | 3.5 |
- 数据库连接与操作 | 1 | 1.5 |
- Flask 环境搭建 | 0.5 | 1 |
- 项目结构搭建 | 4 | 5 |
- 用户注册功能 | 4 | 5 |
- 用户登录功能 | 4 | 5 |
- 项目发布功能 | 6 | 8 |
- 项目查看功能 | 6 | 12 |
- 消息功能功能 | 4 | 4 |
- 收藏功能功能 | 5 | 4.5 |
- 项目管理功能 | 5 | 5.5 |
- 前端与后端联调 | 3 | 2 |
- 项目打包 | 3 | 6 |
- 测试 | 3 | 5 |
- 总计 | 56.5 | 76 |
三、解题思路描述与设计实现说明
1.代码实现思路
后端代码实现思路
- 整个后端项目的代码实现思路如下:
- 框架与库的引入:使用 Flask 框架搭建应用,结合 SQLAlchemy 进行数据库操作,Flask-JWT-Extended 处理用户认证,Flask-CORS 允许跨域请求。
- 应用配置:配置数据库连接,使用 MySQL 数据库,并设置用户名和密码。配置 JWT 密钥以支持用户身份验证和生成访问令牌。
- 数据库初始化:初始化数据库和 JWT 管理器,确保应用能够与数据库进行交互并处理用户认证。
- 蓝图注册:注册蓝图,以组织应用的路由和视图,促进代码的模块化和可维护性。
- SocketIO:配置 SocketIO 以支持实时通信功能,例如聊天功能。
前端代码实现思路
- 前端项目的代码显示思路如下:
- 项目结构:使用 Vue.js 框架构建单页面应用,组件化结构提升可维护性和可复用性。通过路由管理实现不同页面间的切换,增强用户体验。
- 主组件:App.vue 是项目的主组件,包含了导航栏和主体内容。使用
组件动态加载路由对应的页面视图。 - 导航栏设计:导航栏根据用户的登录状态动态显示不同的链接。提供的链接包括主页、创建项目、查看项目、个人资料和聊天等功能。
- 用户认证:在组件的 created 生命周期钩子中检查本地存储中是否存在 JWT Token。若存在 Token,通过 Axios 向后端请求用户资料,并更新组件的 username 和 isLoggedIn 状态。若 Token 无效或不存在,则重定向到登录页面。
- 样式和响应式设计:使用 scoped CSS 定义组件的样式,确保样式只影响当前组件。采用 Flexbox 布局实现导航栏的自适应布局,确保在不同屏幕尺寸下的良好显示。提供了一些基本的动画效果,如导航项的滑入动画和鼠标悬停效果,以提升用户体验。
2.实现的流程图或数据流图
- 数据流图:
- 流程图:
3.重要的的代码片段
- 1.后端:获取最近的联系人
这段代码的初衷,就是想像QQ微信一样,能够快速定位到最近有消息来往的人,或者看到陌生人对你发的消息
#最近聊天
@chat.route('/recent-chats', methods=['GET'])
def get_recent_chats():
user_id = request.args.get('user_id')
# 子查询:获取每个对话中最新的一条消息
latest_messages_subquery = (
db.session.query(
func.max(ChatMessage.timestamp).label('latest_timestamp'),
func.least(ChatMessage.sender_id, ChatMessage.recipient_id).label('user1'),
func.greatest(ChatMessage.sender_id, ChatMessage.recipient_id).label('user2')
)
.filter(or_(
ChatMessage.sender_id == user_id,
ChatMessage.recipient_id == user_id
))
.group_by(func.least(ChatMessage.sender_id, ChatMessage.recipient_id),
func.greatest(ChatMessage.sender_id, ChatMessage.recipient_id))
.subquery()
)
# 查询每个对话的最新聊天记录
recent_chats = (
db.session.query(ChatMessage)
.join(latest_messages_subquery, and_(
ChatMessage.timestamp == latest_messages_subquery.c.latest_timestamp,
func.least(ChatMessage.sender_id, ChatMessage.recipient_id) == latest_messages_subquery.c.user1,
func.greatest(ChatMessage.sender_id, ChatMessage.recipient_id) == latest_messages_subquery.c.user2
))
.order_by(ChatMessage.timestamp.desc())
.all()
)
# 格式化输出
result = []
for message in recent_chats:
# 确定对方用户的 ID
chat_with_user_id = message.sender_id if message.sender_id != int(user_id) else message.recipient_id
chat_with_user = User.query.get(chat_with_user_id)
result.append({
'user_id': user_id, # 当前用户的 ID
'chat_with_id': chat_with_user.id, # 对方用户的 ID
'chat_with': chat_with_user.nickname, # 使用对方用户的昵称
'message': message.message,
'timestamp': message.timestamp
})
return jsonify(result)
- 解释:
1.首先查找每一对发生联系的人,这里创建了一个子查询,获取每个对话中最新的消息时间戳。使用 max(ChatMessage.timestamp) 找到每个对话的最新时间,并用 least 和 greatest 函数确保发送者和接收者的 ID 组合唯一。
2.然后通过将子查询与 ChatMessage 表连接,找出每个对话的最新消息。使用 and_ 确保时间戳和用户 ID 的匹配,并按时间戳降序排序,确保最新消息在前。
3.遍历查询结果,确定对方用户的 ID,并使用 User.query.get(chat_with_user_id) 获取该用户的信息。将聊天相关的信息整理成字典,添加到 result 列表中。 - 2.前端:创建项目(javascript片段)
<script>
import axios from 'axios';
export default {
name: 'CreateProject',
data() {
return {
projectTitle: '',
projectContent: '',
projectCategory: '',
majorType: '',
successMessage: '',
errorMessage: '',
maxTitleLength: 50,
maxContentLength: 500,
selectedFiles: [] // 存储选中的文件
};
},
computed: {
isDisabled() {
return (
!this.projectTitle ||
this.projectTitle.length > this.maxTitleLength ||
!this.projectContent ||
this.projectContent.length > this.maxContentLength ||
!this.projectCategory ||
!this.majorType ||
(this.selectedFiles.length > 9) // 限制图片数量
);
}
},
methods: {
checkTitleLength() {
if (this.projectTitle.length > this.maxTitleLength) {
this.projectTitle = this.projectTitle.slice(0, this.maxTitleLength);
}
},
checkContentLength() {
if (this.projectContent.length > this.maxContentLength) {
this.projectContent = this.projectContent.slice(0, this.maxContentLength);
}
},
handleFileUpload(event) {
this.selectedFiles = Array.from(event.target.files).slice(0, 9); // 获取最多9张图片
},
createProject() {
this.successMessage = '';
this.errorMessage = '';
const formData = new FormData();
formData.append('title', this.projectTitle);
formData.append('content', this.projectContent);
formData.append('category', this.projectCategory);
formData.append('major_type', this.majorType);
this.selectedFiles.forEach(file => {
formData.append('images', file);
});
axios.post('http://localhost:5000/create-project', formData, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
if (response.data.success) {
this.successMessage = response.data.message;
setTimeout(() => {
this.$router.push('/projects');
}, 1500);
} else {
this.errorMessage = response.data.message;
}
})
.catch(error => {
if (error.response) {
this.errorMessage = error.response.data.message;
} else {
this.errorMessage = 'An error occurred while creating the project.';
}
});
}
}
};
</script>
- 解释:
1.isDisabled 计算属性用于判断“创建项目”按钮是否应该被禁用。只有在所有输入字段有效且符合条件时,按钮才会启用。
2.通过 Array.from 将 FileList 转换为数组并使用 slice(0, 9) 限制选择的文件数量,这有助于防止用户超出文件数量的限制。
3.根据响应结果,更新成功或错误消息。如果创建成功,使用 setTimeout 在 1.5 秒后重定向到项目列表页面。
四、附加特点设计与展示
1.项目展示(收于博客园10M的文件大小限制,gif可能会有点糊)
- 主页: 点击按钮后跳转到登陆界面
- 登陆和注册:在登陆界面如果没有注册可以跳转到用户界面,注册界面有用户名(用于账号登陆),二次确认密码,昵称和选择头像功能,登陆页面输入账号密码登陆后会直接跳转到项目查看功能
- 项目查看功能: 每十个项目为一页,显示项目的创建者,项目标签,项目内容等等,点击项目的图片可以进行图片的放大
- 新建项目功能: 可以输入标题,内容,选择项目标签(项目类型和专业类别),上传项目图片
- 标签搜索功能: 点击项目标签会将标签内容自动填入搜索栏内,方便快速搜索同类型标签项目
- 项目搜索功能: 1.选择项目标签进行搜索 2.输入关键词内容进行搜索
- 查看我的资料和编辑资料功能: 如果当前查看的用户是自己则可以编辑资料
- 陌生人聊天: 如果你对一个项目感兴趣可以直接点开创建者的头像查看他的个人信息,并且可以和他进行聊天
- 最近联系人功能:突然找你聊天的陌生人和与你聊过天的人都会显示在这里,并且显示最后的一条消息,点进去即可继续聊天
- 项目管理-创建的项目: 可以管理所有创建的项目,编辑项目信息,删除项目,如果审核参与项目的人员,并且如果有参与项目的人员可以发布公告给他们
- 项目管理-参与的项目: 可以查看所有参与的项目,查看目前自己的的状态,查看参与项目的所有人员
- 收藏的项目: 可以查看收藏的所有项目
- 我的消息: 按钮上可以查看未读消息数,点进去可以查看所有的消息
2.附加特点设计
- 消息通知功能
- 这个功能意义在于当有人申请加入你的项目的时候,需要有一个方式让你感知到,而不是一直盯着项目审核的地方看,同样,当你的申请被通过或者拒绝之后,需要有一个方式让你知道你的申请的结果,与此同时,该功能还可以作为一个项目的群发公告功能,而不是项目创建者一个一个去私聊。
- 设计思路:
- 消息模型: 创建一个 Message 模型,包含字段如 sender_id(发送者ID)、recipient_id(接收者ID)、content(消息内容)、is_read(已读状态)、timestamp(时间戳)等,以便存储和管理消息。
- 消息发送: 当项目创建者审核参与申请、发布公告或其他需要通知用户的操作时,系统会自动生成消息,并将其保存到数据库中。
- 消息获取: 用户可以通过一个接口获取自己的消息列表,消息按照时间戳倒序排列,以便最新的消息优先展示。
- 已读状态管理: 提供接口允许用户将消息标记为已读。确保用户只能标记自己接收的消息为已读。
- 未读消息统计: 提供接口获取当前用户的未读消息数量,帮助用户快速了解待处理的消息。
- 有价值的代码片段
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'))
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'))
content = db.Column(db.String(255), nullable=False)
is_read = db.Column(db.Boolean, default=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
sender = db.relationship('User', foreign_keys=[sender_id])
recipient = db.relationship('User', foreign_keys=[recipient_id])
解释:消息模型定义了消息的基本结构,sender_id 和 recipient_id 使用外键关联用户表,is_read 默认值为 False,表示消息未读。
unread_count = Message.query.filter_by(recipient_id=user_id, is_read=False).count()
return jsonify({'unread_count': unread_count}), 200
解释:该接口查询当前用户的未读消息数量,以便用户快速了解有多少消息需要处理。
五、目录说明和使用说明
1.目录说明
pg
│ .gitignore # Git 忽略文件配置
│ babel.config.js # Babel 编译配置
│ directory.txt # 目录列表文件
│ jsconfig.json # JavaScript 配置文件
│ package-lock.json # 项目依赖锁定文件
│ package.json # 项目依赖及脚本配置
│ README.md # 项目说明文件
│ requirements.txt # Python 项目依赖文件
│ vue.config.js # Vue 项目配置文件
│
├─backend # 后端代码目录
│ │ app.py # 主应用文件
│ │ chat.py # 聊天相关逻辑
│ │ model.py # 数据模型定义
│ │ routes.py # 路由配置
│ │
│ └─__pycache__ # Python 编译缓存目录
│
├─dist # 构建后输出目录
│ │ favicon.ico # 网站图标
│ │ index.html # 入口 HTML 文件
│ │
│ ├─assets # 静态资源文件夹
│ │
│ ├─css # 样式文件夹
│ │
│ ├─img # 图片文件夹
│ │
│ ├─js # JavaScript 文件夹
│ │
│ └─uploads # 上传文件目录
│
├─public # 公共资源目录
│ │ favicon.ico # 网站图标
│ │
│ ├─assets # 静态资源文件夹
│ │
│ └─uploads # 上传文件目录
│
├─src # 前端源代码目录
│ │ App.vue # Vue 主组件
│ │ main.js # 应用入口文件
│ │
│ ├─assets # 前端资源文件夹
│ │
│ └─components # Vue 组件目录
│ ChatMessage.vue # 聊天消息组件
│ CreateProject.vue # 创建项目组件
│ EditUserProfile.vue # 编辑用户资料组件
│ FavoriteProjects.vue # 收藏项目组件
│ HomePage.vue # 首页组件
│ MyMessage.vue # 我的消息组件
│ MyPage.vue # 我的个人页面组件
│ MyparticipateProjects.vue # 我参与的项目组件
│ RecentChats.vue # 最近聊天组件
│ UserLogin.vue # 用户登录组件
│ UserRegister.vue # 用户注册组件
│ ViewMyProjects.vue # 查看我的项目组件
│ ViewProjects.vue # 查看项目组件
│ ViewUserProfile.vue # 查看用户资料组件
│ ViewUsers.vue # 查看用户列表组件
2.项目使用说明
- 前端: 用浏览器打开pg/dist/index.html即可
- 后端:
- 先cd pg/,然后运行pip install -r requirements.txt安装所需依赖
- 然后运行python app.py, 注意:如果是vscode直接点击app.py然后右上角三角形图标运行也要进入pg文件夹下再运行,因为代码里面使用了相对路径,不在pg文件夹下运行有些功能会有问题
- 备注:
- 如果python数据库报错的话,请安装mysql,并且设置root密码为123123,然后创建一个叫demo的数据库
- 具体步骤可以询问AIGC工具,它能提供更详细的说明,或者加我QQ 298668170联系我
六、单元测试
1.测试工具
- 我们选用的测试工具是 Python 的 unittest 模块,这是一个内置的单元测试框架,支持测试用例的创建、执行和结果报告。
- 学习单元测试的过程中,可以参考Bilibili的一些教程,具体写测试的时候可以使用AIGC工具来辅助,AIGC工具可以根据你所说的测试用例快速写出一堆,极大地减少了人工成本
2.测试代码
- 这是注册接口的测试代码
import unittest
from app import app # 确保导入你的 Flask 应用和数据库模型
from model import db
class RegisterTestCase(unittest.TestCase):
def setUp(self):
# 在每个测试之前执行
self.app = app.test_client()
self.app.testing = True
# 创建数据库和表
with app.app_context():
db.create_all()
def tearDown(self):
# 在每个测试之后执行
with app.app_context():
db.session.remove()
db.drop_all()
def test_register_success(self):
response = self.app.post('/register', json={
'username': 'testuser',
'password': 'password123',
'nickname': 'testnickname',
'avatar': '1.png'
})
self.assertEqual(response.status_code, 201)
# 解析 JSON 数据
response_data = response.get_json() # 直接获取 JSON 数据
self.assertEqual(response_data['message'], '用户创建成功') # 检查消息字段
self.assertTrue(response_data['success']) # 检查成功状态
def test_register_username_exists(self):
# 首先注册一个用户
self.app.post('/register', json={
'username': 'testuser',
'password': 'password123',
'nickname': 'testnickname',
'avatar': '1.png'
})
# 再次尝试注册同样的用户名
response = self.app.post('/register', json={
'username': 'testuser',
'password': 'newpassword',
'nickname': 'newnickname',
'avatar': '2.png'
})
self.assertEqual(response.status_code, 200)
self.assertIn(b'Username already exists', response.data)
def test_register_nickname_empty(self):
response = self.app.post('/register', json={
'username': 'newuser',
'password': 'password123',
'nickname': '',
'avatar': '1.png'
})
self.assertEqual(response.status_code, 200)
self.assertIn(b'Nickname cannot be empty', response.data)
def test_register_nickname_exists(self):
# 首先注册一个用户
self.app.post('/register', json={
'username': 'user1',
'password': 'password123',
'nickname': 'unique_nickname',
'avatar': '1.png'
})
# 再次尝试注册同样的昵称
response = self.app.post('/register', json={
'username': 'user2',
'password': 'password456',
'nickname': 'unique_nickname',
'avatar': '2.png'
})
self.assertEqual(response.status_code, 200)
self.assertIn(b'Nickname already exists', response.data)
def test_register_invalid_avatar(self):
response = self.app.post('/register', json={
'username': 'avataruser',
'password': 'password123',
'nickname': 'nickname',
'avatar': 'invalid.png' # 无效的头像
})
self.assertEqual(response.status_code, 200)
self.assertIn(b'Invalid avatar selection', response.data)
if __name__ == '__main__':
unittest.main()
- 这是五个函数的测试说明
-
test_register_success: 测试用户成功注册,验证返回状态码为 201,消息为“用户创建成功”,成功状态为 True。
-
test_register_username_exists: 测试已存在的用户名注册,尝试再次注册同样的用户名,验证返回状态码为 200,消息包含“Username already exists”。
-
test_register_nickname_empty: 测试昵称为空的注册请求,验证返回状态码为 200,消息包含“Nickname cannot be empty”。
-
test_register_nickname_exists: 测试已存在的昵称注册,尝试再次注册同样的昵称,验证返回状态码为 200,消息包含“Nickname already exists”。
-
test_register_invalid_avatar: 测试无效头像的注册请求,验证返回状态码为 200,消息包含“Invalid avatar selection”。
3. 构造测试数据的思路
- 先测试正常功能是否能够实现,然后测试一些特殊的情况,例如用户注册后想要再次注册,或者用户直接不写昵称就要注册,应对测试人员的***难之前应该先想一些极端的情况
七、github记录截图
签入记录
提交记录
八、遇到的代码模块异常及解决方法
1.跨域问题
- 问题描述:在开发过程中遇到了跨域请求(CORS)的问题。当前端应用尝试从不同的源(域名、协议或端口)向后端 API 发送请求时,浏览器会阻止该请求,导致无法获取数据。
- 做过哪些尝试:
- 检查前端路由是否正确:确认请求路径是否正确。
- 后端设置 CORS:
- 在 Flask 中,尝试使用
flask-cors
库来配置 CORS。 - 添加如下代码:
from flask_cors import CORS app = Flask(__name__) CORS(app) # 允许所有域名
- 在 Flask 中,尝试使用
- 在后端设置CORS之后成功解决
- 收获:加深了对跨域资源共享(CORS)机制的理解,了解到安全策略的重要性。
2.前端图片显示问题
- 问题描述:在前端开发中,遇到头像选择界面时,头像图片未能正常显示,导致用户无法选择头像。这影响了用户体验和功能的完整性。
- 做过哪些尝试:
- 检查图片路径:确保头像图片路径是否正确,尝试使用绝对路径和相对路径。
- 使用静态资源管理:尝试将图片放在不同的目录下,并通过不同方式引入(如直接引用 URL)。
- 调试控制台:查看浏览器的控制台,检查是否有 404 错误,确认图片是否成功加载。
- 使用 require:改用
require
动态引入图片,确保 webpack 能够正确处理这些图片文件。
- 最终通过使用
require
动态引入图片的方式成功解决了问题,确保头像能够正确显示。 - 收获:掌握了如何使用
require
动态加载图片资源,提升了对 webpack 的理解
九、评价队友
值得学习的地方
- 是个非常努力的人,可以快速解决问题,可以对设想的前端的样式进行实现,开发中总是有各种新需求也没有抱怨
需要改进的地方
- 对于环境的配置方面,仍然还有需要提升的地方