软件工程第二次结对作业
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/SE2024 |
---|---|
结对同学的博客链接 | https://www.cnblogs.com/XXX-CHEN |
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/SE2024/homework/13281 |
仓库的GitHub项目地址 | https://github.com/hahayehuhei/102201224-102201226 |
学号 | 102201224 |
一、具体分工
本次作业分工如下
陈博涵同学:
前端部分:
- 设计“我的界面”,“项目”界面,“好友界面”以及“私聊界面”
- 实现搜索栏,分类过滤,加入项目和编辑资料的交互功能
后端部分:
- 用node.js和MongoDB实现数据库系统,对用户创建的项目进行存储,实现增删改查功能
- 提供用户更新资料功能
陈潇健同学:
前端部分:
- 设计最初版本css样式文件,设计网页基本风格
- 设计登录,注册,忘记密码界面,实现密码的隐私显示以及对用户输入的邮箱格式正确性检查
后端部分:
- 提供注册、登录、重置密码等功能
- 用bcrypt用于加密和验证用户的密码
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(min) | 实际耗时(min) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
Estimate | 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 600 | 720 |
Analysis | 需求分析(包括学习新技术) | 120 | 150 |
Design Spec | 生成设计文档 | 40 | 35 |
Design Review | 设计复审 | 30 | 30 |
Coding Standard | 代码规范(为开发制定合适的规范) | 15 | 10 |
Design | 具体设计 | 120 | 120 |
Coding | 具体编码 | 180 | 180 |
Code Review | 代码复审 | 60 | 60 |
Test | 测试(自我测试,修改,提交修改) | 180 | 120 |
Reporting | 报告 | 30 | 30 |
Test Report | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 30 | 30 |
合计 | 1495 | 1415 |
三、解题思路描述与设计实现说明
1.代码实现思路
问题描述:
要构建一个名为“ProjectPartner”的平台,允许用户发布项目、招募项目伙伴,并且能够与项目成员进行实时沟通交流。用户能够在平台上讨论项目实施方案,表达想法。
需求分析:
- 项目发布和招募功能:用户能够通过平台发布项目,同时开放招募项目伙伴的选项。需要提供便捷的项目发布界面,并让用户选择项目类型,上传资料。
- 注册和登录系统:用户需要注册并登录系统才能发布项目、加入项目和聊天。必须支持密码加密和身份验证,保证安全性。
- 用户与项目管理:用户可以管理自己发布的项目和已加入的项目,更新项目状态,并能随时退出项目。
- 修改个人资料功能:用户可以在“我的资料”页面修改自己的用户名、邮箱、专业、个人简介和头像。资料修改后需即时保存并在个人页面更新展示。
解决思路:
前端部分:
- 用户界面设计:使用HTML、CSS、JavaScript开发直观的用户界面。包括项目发布、项目列表、项目详情、聊天窗口等页面。UI界面设计要简洁,确保用户体验友好。
- 交互功能实现:通过JavaScript和AJAX实现动态交互,如项目发布后自动更新列表,实时显示新消息。提供搜索、过滤项目功能,方便用户快速找到感兴趣的项目。
- 实时聊天功能:在前端使用Socket.io或WebSocket连接,与后端保持实时通信,实现即时消息发送和接收。聊天记录需要持续展示在界面上,用户发送消息后自动更新。
后端部分:
- 用户认证和安全:使用Node.js和Express处理用户注册和登录功能。密码加密使用bcrypt,身份验证使用JWT,确保用户数据和隐私安全。后端为所有API提供身份验证中间件。
- 项目和用户数据管理:后端负责存储项目和用户信息,使用MongoDB存储用户、项目和聊天记录数据。通过API让前端可以发布、获取项目详情、查询项目等。用户加入项目后,将项目保存到他们的项目列表。
2.流程图
项目信息数据流图
3.有价值的代码片段
3.1发布项目和实时获取项目功能
前端代码分析:
项目发布:
- 用户通过表单输入项目名称、图片URL、描述和类型。
- form标签通过id="publish-project-form"进行识别,并使用JavaScript绑定submit事件。
<form id="publish-project-form">
<label for="project-name">项目名称:</label>
<input type="text" id="project-name" required>
<!-- 其他输入字段... -->
<button type="submit">发布项目</button>
</form>
表单提交:
- 使用JavaScript监听表单的提交事件,防止表单默认提交行为(event.preventDefault())。
- 从表单中提取用户输入的项目数据并构造成JSON对象。
- 通过fetch向后端API发送POST请求,API路径为http://localhost:5000/api/projects,发送新项目数据。
document.getElementById('publish-project-form').addEventListener('submit', function(event) {
event.preventDefault();
const newProject = {
name: document.getElementById('project-name').value,
image: document.getElementById('project-image').value,
description: document.getElementById('project-description').value,
type: document.getElementById('project-type').value
};
fetch('http://localhost:5000/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProject)
})
.then(response => response.ok ? response.json() : Promise.reject('发布项目失败'))
.then(project => {
const userProjects = JSON.parse(localStorage.getItem('userProjects')) || [];
userProjects.push(project);
localStorage.setItem('userProjects', JSON.stringify(userProjects));
alert('项目发布成功!');
window.location.href = 'projects.html';
})
.catch(err => alert(err));
});
本地存储更新:
- 项目发布成功后,使用localStorage存储用户发布的项目。
- 成功发布后,用户被重定向到projects.html,查看已发布的项目。
后端代码分析:
发布新项目的API(POST /api/projects):
- 后端通过POST /projects接收发布请求,并将新项目存储到数据库。
- 新的项目会从请求体中接收项目名称、图片、描述和类型,并将其保存到数据库。
router.post('/projects', async (req, res) => {
const { name, image, description, type } = req.body;
const project = new Project({
name, image, description, type, isDefault: false
});
try {
const newProject = await project.save();
res.status(201).json(newProject);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
获取项目的API(GET /api/projects):
- 通过GET /projects请求,后端从数据库获取所有项目,并将结果返回给前端,供项目列表展示。
- 该功能用于在项目页面展示所有用户和系统自带的项目。
router.get('/projects', async (req, res) => {
try {
const projects = await Project.find();
res.json(projects);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
3.2实现注册、登录、找回密码和验证密码功能
注册功能
前端代码分析:
- 用户通过注册表单输入用户名、工学号、密码和邮箱。
- 前端通过fetch发出POST请求,将用户的注册信息发送到后端。
<form id="register-form" onsubmit="event.preventDefault(); handleRegister();">
<label for="new-username">用户名:</label>
<input type="text" id="new-username" required>
<label for="student-id">工学号:</label>
<input type="text" id="student-id" required>
<label for="new-password">密码:</label>
<input type="password" id="new-password" required>
<label for="email">邮箱:</label>
<input type="email" id="email" required>
<button type="submit">注册</button>
</form>
后端代码分析:
- 使用bcrypt.hash()对用户输入的密码进行加密,以确保存储在数据库中的密码安全。
- 在数据库中,用户名必须唯一,系统会检查用户是否已注册。
app.post('/api/register', async (req, res) => {
const { username, studentId, password, email } = req.body;
// 检查用户名是否已存在
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).send('用户名已存在');
}
// 密码加密并存储
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ username, studentId, password: hashedPassword, email });
await newUser.save();
res.status(201).send('用户注册成功');
});
登录功能
前端代码分析:
- 用户通过登录表单输入用户名和密码,前端通过fetch发起登录请求。
<form id="login-form" onsubmit="event.preventDefault(); handleLogin();">
<label for="username">用户名:</label>
<input type="text" id="username" required>
<label for="password">密码:</label>
<input type="password" id="password" required>
<button type="submit">登录</button>
</form>
后端代码分析:
- 通过bcrypt.compare()验证输入的密码和数据库中的加密密码是否匹配。
*如果验证成功,生成JWT(JSON Web Token),并将其返回给前端,用于后续的身份验证。
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
// 验证密码
if (user && await bcrypt.compare(password, user.password)) {
const token = jwt.sign({ id: user._id }, 'your_jwt_secret');
res.json({ token });
} else {
res.status(401).send('用户名或密码错误');
}
});
找回密码功能
前端代码分析:
- 表单要求用户输入用户名、学号、邮箱及新密码。
- 前端检查新密码和确认密码是否一致,然后通过fetch发出POST请求,发送重置密码请求。
<form id="reset-password-form" onsubmit="event.preventDefault(); handleResetPassword();">
<label for="username">用户名:</label>
<input type="text" id="username" required>
<label for="student-id">工学号:</label>
<input type="text" id="student-id" required>
<label for="email">邮箱:</label>
<input type="email" id="email" required>
<label for="new-password">新的密码:</label>
<input type="password" id="new-password" required>
<label for="confirm-password">确认密码:</label>
<input type="password" id="confirm-password" required>
<button type="submit">重置密码</button>
</form>
后端代码分析:
- 验证用户提供的用户名、学号和邮箱是否匹配数据库中的记录。
- 如果匹配成功,将新密码加密后存入数据库,更新用户的密码。
app.post('/api/reset-password', async (req, res) => {
const { username, studentId, email, newPassword } = req.body;
const user = await User.findOne({ username, studentId, email });
if (user) {
const hashedPassword = await bcrypt.hash(newPassword, 10);
user.password = hashedPassword;
await user.save();
res.send('密码已重置');
} else {
res.status(404).send('用户未找到');
}
});
3.3实现编辑资料功能
前端编辑资料代码功能:
- 表单收集用户的基本信息,包括昵称、邮箱、专业、个人简介和头像URL。
- 表单字段设置为required以确保用户必须填写某些信息,从而提高数据的完整性。
<main>
<h2>编辑资料</h2>
<form id="edit-profile-form">
<label for="username">昵称:</label>
<input type="text" id="username" name="username" required>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
<label for="major">专业:</label>
<input type="text" id="major" name="major">
<label for="bio">个人简介:</label>
<textarea id="bio" name="bio"></textarea>
<label for="avatar">头像URL:</label>
<input type="text" id="avatar" name="avatar">
<button type="submit">保存</button>
</form>
</main>
获取用户现有资料:
- 使用fetch向后端发出GET请求,获取当前用户的资料。
- 通过JWT验证用户身份,确保只有已登录的用户可以访问其资料。
- 一旦获取成功,表单字段将自动填充用户的现有信息,方便用户进行修改。
fetch('http://localhost:5000/api/profile', {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` // 使用JWT token
}
})
.then(response => {
if (!response.ok) {
throw new Error('获取用户资料失败');
}
return response.json();
})
.then(data => {
document.getElementById('username').value = data.username;
document.getElementById('email').value = data.email;
document.getElementById('major').value = data.major || '';
document.getElementById('bio').value = data.bio || '';
document.getElementById('avatar').value = data.avatar || '';
})
.catch(err => {
console.error('获取用户资料失败:', err);
});
提交编辑资料表单:
- 使用event.preventDefault()阻止表单的默认提交行为,以使用JavaScript自定义处理。
- 收集用户输入的修改信息并构建一个对象,通过PUT请求将其发送到后端进行更新。
- 如果更新成功,用户会看到成功提示,并自动重定向到资料页面。
document.getElementById('edit-profile-form').addEventListener('submit', function(event) {
event.preventDefault(); // 阻止默认提交
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const major = document.getElementById('major').value;
const bio = document.getElementById('bio').value;
const avatar = document.getElementById('avatar').value;
fetch('http://localhost:5000/api/update-profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ username, email, major, bio, avatar }) // 将数据发送到服务器
})
.then(response => {
if (response.ok) {
alert('资料更新成功');
window.location.href = 'mine.html'; // 更新成功后重定向到资料页面
} else {
alert('更新资料失败');
}
})
.catch(err => {
console.error('更新资料出错:', err);
alert('更新资料出错');
});
});
后端更新用户资料:
- 通过JWT验证中间件确认用户身份,确保只有授权用户才能更新自己的资料。
- 查找用户后,更新其资料,并将更改保存到数据库。
- 提供相应的成功或失败反馈,以便于前端进行适当的响应处理。
app.put('/api/update-profile', authenticateToken, async (req, res) => {
const { username, email, major, bio, avatar } = req.body;
const userId = req.userId; // 通过JWT中间件获取的用户ID
try {
const user = await User.findById(userId);
if (!user) return res.status(404).send('用户未找到');
// 更新用户信息
user.username = username;
user.email = email;
user.major = major;
user.bio = bio;
user.avatar = avatar;
await user.save();
res.send('资料更新成功');
} catch (err) {
res.status(500).send('更新资料失败');
}
});
四、附加特点设计与展示
-
登陆界面
若是账号密码输入错误则登陆失败
-
注册界面
可以检测出在注册时填写邮箱的格式是否正确
-
忘记密码界面
新的密码和确认密码不一致,则提示错误
该用户不存在,则提示错误
-
主页面
搜索功能
过滤功能
加入项目、删除项目功能
发布项目、编辑项目功能
好友功能以及私聊功能
编辑资料功能
五、目录说明和使用说明
目录说明
/project
│
├── images // 存放项目图片
│ ├── project1.jpg // 高等数学研究项目的图片
│ ├── project2.jpg // 福大建筑美学项目的图片
│ ├── project3.jpg // 风景园林规划实训项目的图片
│ ├── project4.jpg // 孙子兵法实操项目的图片
│ ├── project5.jpg // 嵌入式系统原理研究项目的图片
│ ├── project6.jpg // 企鹅生活习性观察项目的图片
│ ├── avatar1.jpg // 好友1的头像
│ └── avatar2.jpg // 好友2的头像
│
├── node_modules // 存放项目依赖的Node.js模块
│
├── routes // 存放路由文件
│ ├── projectRoutes.js // 处理项目相关API的路由
│ └── add.js // 数据库初始化和项目添加的逻辑
│
├── models // 存放数据模型
│ ├── Project.js // 项目模型
│ └── user.js // 用户模型
│
├── project-detail.html // 项目详情页面
├── server.js // 后端服务器启动文件
├── projects.html // 项目列表页面
├── publish-project.html // 发布新项目页面
├── package.json // 项目依赖描述文件
├── package-lock.json // 锁定依赖版本的文件
├── main.html // 主页
├── login.html // 登录页面
├── mine.html // 我的资料页面
├── friends.html // 好友列表页面
├── chat.html // 私聊页面
├── forget.html // 忘记密码页面
├── register.html // 注册页面
└── edit-profile.html // 编辑资料页面
└── default-project-detail.html // 默认项目详情页面
└── style.css // 样式文件
使用说明
1.在我们的github仓库下载源代码,进行解压缩。
将依赖文件压缩包node_modules.zip压缩到project文件夹内
2.安装node.js
要安装 Node.js,首先访问 Node.js 官方网站,下载适合您操作系统的安装包(Windows、macOS 或 Linux)。在 Windows 上,运行 .msi 文件并按照提示安装,确保勾选Add to PATH选项;在 macOS 上,您可以使用 Homebrew 安装,命令为 brew install node;在 Linux 上,可以使用包管理器(如 apt 或 yum)或 NodeSource 安装。安装完成后,通过在终端或命令提示符中运行 node -v 和 npm -v 来验证安装是否成功。
3.安装mongodb
要安装 MongoDB,首先访问 MongoDB 官方网站,选择适合您操作系统的 MongoDB Community Server 安装包。在 Windows 上,下载 .msi 文件并运行安装向导,按照提示进行安装,并确保选择Install MongoDB as a Service选项;在 macOS 上,您可以使用 Homebrew,命令为 brew tap mongodb/brew,然后执行 brew install mongodb-community;在 Linux 上,可以使用包管理器(如 apt 或 yum)或按照 MongoDB 文档提供的步骤进行安装。安装完成后,您可以通过在终端或命令提示符中运行 mongod 启动 MongoDB 服务,接着使用 mongo 命令连接到 MongoDB 数据库。
4.运行服务器
以管理员身份运行cmd,输入命令net start MongoDB来启动MongoDB服务
再打开cmd,进入project文件夹,输入node server.js以启动服务器
*此时可以正常运行和使用网页了
5.运行测试用例
打开cmd在project路径下输入npm test即可使用测试用例
六、单元测试
选用的测试工具
- 我们选用的测试工具是 Jest 和 Supertest。Jest 是一个功能强大的 JavaScript 测试框架,适用于单元测试和集成测试。Supertest 是一个用于测试 HTTP 服务器的库,通常与 Jest 一起使用,以便对 API 进行端到端测试。
学习单元测试的简易教程
项目部分单元测试代码示例
- 以下是一个简单的单元测试示例,测试用户注册和登录功能:
// user.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('./server'); // 确保路径正确,指向你的 server.js 文件
const User = require('./models/User'); // 引入用户模型
// 在测试开始前连接到 MongoDB
beforeAll(async () => {
if (mongoose.connection.readyState === 0) {
await mongoose.connect('mongodb://localhost:27017/project_manager_test');
}
});
// 在每个测试后清理数据库
afterEach(async () => {
await User.deleteMany({});
});
// 在测试结束后断开 MongoDB 连接
afterAll(async () => {
await mongoose.connection.close();
});
// 测试用例
describe('用户注册和登录', () => {
it('应该成功注册用户', async () => {
const response = await request(app)
.post('/api/register')
.send({
username: 'testuser',
studentId: '123456',
password: 'password123',
email: 'testuser@example.com'
});
expect(response.status).toBe(201);
expect(response.text).toBe('用户注册成功');
});
it('应该成功登录用户', async () => {
await request(app)
.post('/api/register')
.send({
username: 'testuser',
studentId: '123456',
password: 'password123',
email: 'testuser@example.com'
});
const response = await request(app)
.post('/api/login')
.send({
username: 'testuser',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
});
it('登录时应返回401错误,用户名或密码错误', async () => {
const response = await request(app)
.post('/api/login')
.send({
username: 'wronguser',
password: 'wrongpassword'
});
expect(response.status).toBe(401);
expect(response.text).toBe('用户名或密码错误');
});
});
测试的函数说明
*用户注册: 测试用户是否能够成功注册,并检查返回状态和消息。
*用户登录: 测试用户是否能够成功登录,并检查返回的 token。
*错误登录: 测试当提供错误的用户名或密码时,是否返回 401 错误。
构造测试数据的思路
我考虑了多种情况:
- 正常情况: 创建有效的用户数据,确保测试能够通过。
- 边界情况: 测试用户名、密码和电子邮件的边界条件,例如最小和最大长度。
- 无效数据: 测试无效的用户名、密码和电子邮件格式,确保系统能够正确处理这些情况。
- 重复数据: 测试注册时使用已存在的用户名或电子邮件,确保系统能够返回适当的错误消息。
- 异常情况: 模拟服务器错误或数据库连接失败的情况,确保系统能够优雅地处理这些异常。
如何考虑将来测试人员的***难
从以下几个维度考虑:
- 全面性: 确保测试覆盖所有可能的输入情况,包括边界值和无效值。
- 可维护性: 编写清晰、易于理解的测试用例,以便其他测试人员能够快速上手。
- 文档: 提供详细的测试文档,说明每个测试用例的目的和预期结果,帮助测试人员理解测试逻辑。
- 灵活性: 设计测试用例时考虑到未来可能的功能扩展,确保测试能够适应代码的变化。
七、Github的代码签入记录截图
八、遇到的代码模块异常或结对困难及解决方法
问题描述
- 在项目开发过程中,我在实现用户发布新项目的功能时遇到了数据存储问题。当用户尝试将新项目保存到MongoDB数据库时,系统并未成功创建项目,导致用户无法正常发布项目。这不仅影响了项目的功能完整性,也对用户体验造成了困扰。
做过哪些尝试
检查MongoDB连接:
- 首先,我确认了MongoDB服务是否正常运行,并确保在代码中使用的连接字符串正确指向了本地数据库。
- 使用mongo命令检查数据库连接是否成功,确保能够访问project_manager数据库。
审查数据模型:
- 我检查了MongoDB中的Project模型定义,确保所有字段的类型和必填项设置正确,特别注意到name、image、description和type字段的必填属性。
确保项目模型与前端表单中的数据结构一致,避免因字段不匹配而导致存储失败。
调试发布请求: - 我在前端提交项目的逻辑中添加了调试信息,打印出即将发送的请求体,以确认发送到服务器的数据格式是否正确。
使用console.log(newProject)打印了新项目的内容,确保包含所有必要的字段。
检查后端路由:
- 在后端router.post('/projects')中,确保对请求体的解析没有问题,使用console.log(req.body)打印出接收到的数据,确保数据结构符合预期。
捕捉错误信息:
- 在保存项目的代码中添加了错误处理,确保在出现异常时可以得到明确的错误信息,帮助定位问题。
是否解决
经过多次调试和调整,我终于解决了数据存储的问题。最终,我发现之前发送的请求中缺少了一些必要的字段,导致MongoDB拒绝保存。因此,在确保数据完整性的同时,成功将项目保存到了数据库中。
有何收获
这次调试经历让我深刻体会到在开发过程中,细节的严谨性至关重要。通过系统化地检查每一个环节,从数据库连接到数据模型、请求体的结构,我不仅学会了如何更有效地排查问题,还提升了对前后端交互的理解。这次经历使我更加注重数据的完整性和一致性,为未来的项目开发打下了坚实的基础。
评价
评价我的结对伙伴陈潇健
- 我的伙伴陈潇健在项目中表现出色,尤其在沟通方面,在开发过程中他总是能够清晰地表达自己的想法,并积极参与讨论。在技术能力上,他对相关技术的掌握非常扎实,能够独立解决问题。在团队合作方面,他乐于帮助他人,并且始终关注团队的共同目标,确保任务的顺利完成。总体来说,他在项目中做出了重要贡献。
该项目的不足之处
- 项目的元素过于单一,不够贴近实际生活的要求
- 未能真正实现即时通讯的功能
- 没有个性化推荐项目的功能,在数据库里仅实现了简单的增删改查
- 代码目录结构不够有条理,应该建立不同的文件夹来管理实现不同功能的模块