XXX-CHEN

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/SE2024
结对同学的博客链接 https://www.cnblogs.com/wudichaohouxia
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/SE2024/homework/13281
仓库的GitHub项目地址 https://github.com/hahayehuhei/102201224-102201226
学号 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的代码签入记录截图



八、遇到的代码模块异常或结对困难及解决方法

问题描述

  • 1.在项目开发过程中,我在实现用户登录注册和忘记密码功能时点击相应按钮后网页没有任何响应
  • 2.在项目开发过程中,我在实现用户注册时发现注册同一用户名时mongodb会直接停止运行

做过哪些尝试

对于问题1

检查JavaScript错误:

  • 我打开浏览器的开发者工具,查看控制台是否有任何 JavaScript 错误。

确保后端服务器正在运行:

  • 我检查了服务器正在运行,并且没有错误。

检查 API 路径:

  • 在前端代码中,fetch 请求的 URL 是 /XXX/XXXXX。确保服务器在正确的端口上监听这个路径。默认情况下,前端代码会尝试在当前主机和端口上发送请求。如果前端和后端在不同的端口上运行,需要确保请求的 URL 是正确的。

捕捉错误信息:

  • 在保存项目的代码中添加了错误处理,确保在出现异常时可以得到明确的错误信息,帮助定位问题。

对于问题2

先判断错误原因:

  • 在我的用户模型中,username 字段被定义为唯一。这意味着每个用户名必须是唯一的,不能重复。
  • 当我尝试注册一个新用户时,如果提供的用户名已经存在于数据库中,就会抛出这个错误。

检查用户名:

  • 在注册新用户之前,先检查数据库中是否已经存在该用户名。可以在注册路由中添加一个检查逻辑

处理错误:

  • 前端处理注册请求时,确保能够接收到并处理后端返回的错误信息。例如,如果用户名已存在,前端可以显示相应的提示。

测试不同的用户名:

  • 在注册时,尝试使用不同的用户名,确保它们是唯一的。

是否解决

经过多次调试和调整,我发现URL路径错误和用户名的唯一性,我在代码上进行了一定的修改,终于解决了这两个问题。

有何收获

这次调试经历让我深刻体会到开发过程中对细节的重视是至关重要的。我不仅提升了问题排查的能力,还加深了对前后端交互的理解。并且我还我深刻体会到了团队合作的重要性。与搭档的紧密合作让我能够相互补充知识,迅速解决问题。讨论和交流不仅帮助我们找到更有效的解决方案,还激发了更多创意。我认识到,良好的沟通是成功的关键。我们在编写代码时保持持续对话,确保彼此理解对方的思路。这种互动提高了代码质量,也增强了团队默契。

评价

评价我的结对伙伴陈博涵

  • 在这次结对编程中,我的伙伴表现出色,给我留下了深刻的印象。他不仅具备扎实的技术能力,还展现了出色的沟通技巧。每当我们遇到挑战时,他/她总能迅速分析问题,并提出有效的解决方案。
    我特别欣赏他的耐心和乐于分享的态度。在讨论代码时,他总是愿意解释自己的思路,帮助我更好地理解复杂的概念。这种开放的交流方式不仅提升了我们的合作效率,也让我在技术上有了更大的进步。
    此外,他在团队合作中展现出的积极性和责任感也让我倍感鼓舞。无论是主动承担任务,还是在关键时刻提供支持,他都表现得非常出色。这样的伙伴让整个结对编程的过程更加顺畅和愉快。
    总的来说,我非常感激能与这样一位优秀的伙伴合作。

该项目的不足之处

  • 项目的元素过于单一,不够贴近实际生活的要求
  • 未能真正实现即时通讯的功能
  • 没有个性化推荐项目的功能,在数据库里仅实现了简单的增删改查
  • 代码目录结构不够有条理,应该建立不同的文件夹来管理实现不同功能的模块
posted on 2024-10-10 21:33  XXX-CHEN  阅读(24)  评论(0)    收藏  举报