wudichaohouxia

导航

软件工程第二次结对作业

这个作业属于哪个课程 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 进行端到端测试。

学习单元测试的简易教程

  • 阅读文档: 首先,阅读 JestSupertest 的官方文档,了解它们的基本用法和功能。
  • 编写简单测试: 从简单的函数开始,编写测试用例,逐步增加复杂性。

项目部分单元测试代码示例

  • 以下是一个简单的单元测试示例,测试用户注册和登录功能:
// 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拒绝保存。因此,在确保数据完整性的同时,成功将项目保存到了数据库中。

有何收获

这次调试经历让我深刻体会到在开发过程中,细节的严谨性至关重要。通过系统化地检查每一个环节,从数据库连接到数据模型、请求体的结构,我不仅学会了如何更有效地排查问题,还提升了对前后端交互的理解。这次经历使我更加注重数据的完整性和一致性,为未来的项目开发打下了坚实的基础。

评价

评价我的结对伙伴陈潇健

  • 我的伙伴陈潇健在项目中表现出色,尤其在沟通方面,在开发过程中他总是能够清晰地表达自己的想法,并积极参与讨论。在技术能力上,他对相关技术的掌握非常扎实,能够独立解决问题。在团队合作方面,他乐于帮助他人,并且始终关注团队的共同目标,确保任务的顺利完成。总体来说,他在项目中做出了重要贡献。

该项目的不足之处

  • 项目的元素过于单一,不够贴近实际生活的要求
  • 未能真正实现即时通讯的功能
  • 没有个性化推荐项目的功能,在数据库里仅实现了简单的增删改查
  • 代码目录结构不够有条理,应该建立不同的文件夹来管理实现不同功能的模块

posted on 2024-10-10 20:56  无敌超猴侠  阅读(12)  评论(0编辑  收藏  举报