164
社区成员
发帖
与我相关
我的任务
分享| Course of the Assignment | EE308FZ Software Engineering |
|---|---|
| Assignment Requirements | Assignment 5 - Alpha Sprint (Backend Group Sprint Log) |
| Objectives of This Assignment | 记录后端组在冲刺阶段的 API 接口调试、Bug 修复、登录安全优化与数据库功能完善进展 |
| Other References | 阿里巴巴Java开发手册终极版v1.3.0、微信小程序开发文档、Swagger 官方文档、bcrypt 加密文档、Express.js开发指南、MySQL参考手册、JWT认证规范 |
| Project/Group Name | Group 2 —— PoopCare——Backend Group |
| Backend Group Members | 李炳言、苏子妍、王洛森 |
1.API 接口调试与 Bug 修复(文档路由保护异常、路由端点不匹配等)
2.登录功能安全优化(密码加密存储、验证机制完善)
3.数据库连接池实现与连接测试
4.数据库清空工具开发(支持交互式数据清理)
5.前端 API 请求配置适配(端口与 URL 硬编码)
本期冲刺计划工时30h(优化点1预计3h,优化点2预计2h,优化点3预计4h,优化点4预计8h,优化点5预计5h,优化点6预计8h),实际消耗27h,燃尽图数据如下:
| 任务阶段 | 计划剩余工时(h) | 实际剩余工时(h) | 负责成员 | 阶段说明 |
|---|---|---|---|---|
| 冲刺开始前 | 30 | 30 | — | 所有任务待开展 |
| 优化点1完成后 | 27 | 28 | 李炳言 | 计划3h,实际耗时2h |
| 优化点2完成后 | 25 | 26 | 苏子妍 | 计划2h,实际耗时2h |
| 优化点3完成后 | 21 | 22 | 李炳言 | 计划4h,实际耗时4h |
| 优化点4完成后 | 13 | 14 | 苏子妍 | 计划8h,实际耗时8h |
| 优化点5完成后 | 8 | 9 | 李炳言 | 计划5h,实际耗时5h |
| 优化点6完成后 | 0 | 3 | 王洛森 | 计划8h,实际耗时6h(整体低于总计划) |

| 优化点编号 | 优化描述 | 耗时 | 核心原因 | 优化方案核心思路 |
|---|---|---|---|---|
| 优化点1 | API文档路由被认证中间件保护,无法公开访问 | 2h | 路由配置未排除认证中间件,不符合文档公开需求 | 创建Swagger配置,添加独立公开路由,绕过认证保护 |
| 优化点2 | 前端复数端点(/health-records)与后端单数路由(/health-record)不匹配 | 2h | 前端字段名(time/typeIndex等)与后端模型字段名(record_time/shape等)不一致,导致数据验证失败 | 修改前端请求URL,适配后端单数路由格式 |
| 优化点3 | getHealthRecords函数参数与路由传递参数不匹配,导致SQL执行错误 | 4h | 路由传递多余record_type参数,SQL占位符与参数数量不一致 | 移除多余参数,完善函数参数处理与日志调试 |
| 优化点4 | 登录功能缺乏密码加密存储与安全保护机制 | 8h | 明文密码存在泄露风险,无验证码频率限制 | 用bcrypt加密密码,实现密码验证与60秒验证码限流 |
| 优化点5 | 数据库连接管理不完善,无连接池与连接测试功能 | 5h | 单次连接模式性能开销大,无法快速验证连接状态 | 实现MySQL连接池,配置核心参数,提供连接测试方法 |
| 优化点6 | 缺乏便捷数据库清空工具,手动清理需处理外键约束 | 6h | 手动操作繁琐易出错,无批量清理能力 | 开发交互式工具,支持全量/指定表清空,自动处理外键 |
###3.3 后端部分代码
我们上传相关修改代码在github的合作库上

链接:https://github.com/Jupiter-rids/PoopCare-backend
核心问题:API文档路由(/api/docs)被项目全局认证中间件保护,非登录状态下无法访问,不符合API文档公开查阅与接口调试的预期需求。
影响范围: - 开发人员接口调试效率 - 前后端对接时的接口查阅便利性
修复前:无Swagger配置与公开路由,文档路由被认证保护 修复后(核心代码):
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'PoopCare API文档',
version: '1.0.0',
description: 'PoopCare项目后端API接口调试文档'
},
servers: [
{
url: 'http://localhost:3000/api'
}
]
},
apis: ['./routes/*.js', './controllers/*.js'] // 指定API注释所在文件
};
const specs = swaggerJsdoc(options);
module.exports = specs;
const router = express.Router();
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('../config/swagger');
// 公开API文档路由,无需认证
router.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
module.exports = router;
| 对比项 | 修复前(Sequelize ORM) | 修复后(mysql2原生SQL) |
|---|---|---|
| 访问权限 | 需登录认证(401未授权) | 公开访问(无需登录) |
| 访问地址 | 无独立文档路由 | http://localhost:3000/api/docs |
| 响应状态 | 401 Unauthorized | 200 OK |
| 核心功能 | 无可视化文档 | 支持接口列表、参数说明、在线调试 |

核心问题:前端添加健康记录时使用复数形式API端点(/health-records),但后端路由配置为单数形式(/health-record),导致请求地址不匹配,数据无法正常提交。
影响范围:
/health-records(复数) /health-record(单数) 修复前代码(前端addRecord.vue):
const submitRecord = async () => {
try {
const response = await axios.post(`${$apiBaseUrl}/health-records`, recordData);
// 后续逻辑...
} catch (error) {
console.error('提交失败:', error);
}
};
修复后代码:
const submitRecord = async () => {
try {
const response = await axios.post('http://localhost:3000/api/health-record', recordData);
// 后续逻辑...
} catch (error) {
console.error('提交失败:', error);
}
};
| 对比项 | 修复前 | 修复后 |
|---|---|---|
| 前端请求URL | /health-records(复数) | /health-record(单数) |
| 后端路由匹配 | 不匹配(404错误) | 匹配(200成功) |
| 数据提交结果 | 提交失败 | 提交成功 |
| 前后端一致性 | 不一致 | 完全一致 |
核心问题:路由文件调用getHealthRecords函数时传递了多余的record_type参数,而函数未处理该参数,且SQL查询中占位符与实际传递参数数量不匹配,导致执行错误(Error: Incorrect arguments to mysqld_stmt_execute)。
影响范围:
getHealthRecords(userId, { record_type: 'poop' }),传递多余参数 修复前(路由文件 backend/routes/health-record.js)
// 错误:传递多余 record_type 参数
router.get('/', authMiddleware, async (req, res) => {
const result = await healthRecordController.getHealthRecords(req.user.id, { record_type: 'poop' });
if (result.success) {
res.json(result);
} else {
res.status(400).json(result);
}
});
修复前(函数实现 backend/controllers/healthRecordController.js)
exports.getHealthRecords = async (userId, queryParams) => {
try {
const { start_date, end_date, page = 1, limit = 20 } = queryParams;
const sql = `
SELECT * FROM poop_records
WHERE user_id = ?
${start_date ? 'AND time >= ?' : ''}
${end_date ? 'AND time <= ?' : ''}
ORDER BY time DESC
LIMIT ?, ?
`;
const params = [userId];
if (start_date) params.push(start_date);
if (end_date) params.push(end_date);
params.push((page - 1) * limit, limit);
const [rows] = await pool.execute(sql, params);
return { success: true, data: rows };
} catch (error) {
console.error('获取健康记录失败:', error);
return { success: false, message: '获取健康记录列表失败' };
}
};
修复后(路由文件 backend/routes/health-record.js)
// 修复:仅传递必要参数
router.get('/', authMiddleware, async (req, res) => {
const result = await healthRecordController.getHealthRecords(req.user.id, req.query);
if (result.success) {
res.json(result);
} else {
res.status(400).json(result);
}
});
修复后(函数实现 backend/controllers/healthRecordController.js)
exports.getHealthRecords = async (userId, queryParams) => {
try {
// 添加日志调试,明确参数接收情况
console.log('getHealthRecords 参数:', { userId, queryParams });
const {
start_date, end_date, page = 1, limit = 20,
sort_by = 'time', sort_order = 'desc'
} = queryParams;
// 处理排序字段白名单,防止 SQL 注入
const validSortFields = ['time', 'created_at', 'type_index', 'mood_index'];
const sortField = validSortFields.includes(sort_by) ? sort_by : 'time';
const sortDir = sort_order.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
// 构建 SQL 查询,确保占位符与参数数量一致
const sql = `
SELECT * FROM poop_records
WHERE user_id = ?
${start_date ? 'AND time >= ?' : ''}
${end_date ? 'AND time <= ?' : ''}
ORDER BY ${sortField} ${sortDir}
LIMIT ?, ?
`;
const params = [userId];
if (start_date) params.push(new Date(start_date));
if (end_date) params.push(new Date(end_date));
params.push((parseInt(page) - 1) * parseInt(limit), parseInt(limit));
console.log('SQL 查询参数:', params);
const [rows] = await pool.execute(sql, params);
return { success: true, data: rows };
} catch (error) {
console.error('获取健康记录失败:', error);
return { success: false, message: '获取健康记录列表失败', error: error.message };
}
};
| 对比项 | 修复前 | 修复后 |
|---|---|---|
| 参数传递 | 传递多余 record_type 参数 | 仅传递必要的 query 参数 |
| 日志调试 | 无日志,难以排查问题 | 输出参数与 SQL 信息,便于调试 |
| SQL 执行 | 占位符与参数不匹配(400 错误) | 匹配正常,执行成功(200) |
| 功能支持 | 仅支持固定查询条件 | 支持日期筛选、分页、排序,功能更完善 |
核心问题:原登录功能存在安全隐患,具体包括:密码可能以明文形式存储、缺乏严格的密码验证流程、验证码发送无频率限制,易遭受恶意攻击。
影响范围:
定位工具与方法:用户模型代码分析、登录流程梳理
具体问题定位:
修复目标:实现密码安全存储、严格密码验证与账户保护机制
核心修复思路:
修复前(用户模型backend/models/User.js):
// 可能的明文密码存储
async create(userData) {
try {
const sql = `
INSERT INTO users (id, phone, nickname, password, created_at)
VALUES (?, ?, ?, ?, ?)
`;
const params = [uuidv4(), userData.phone, userData.nickname, userData.password, new Date()];
await pool.execute(sql, params);
return { success: true };
} catch (error) {
console.error('创建用户失败:', error);
return { success: false };
}
}
修复前(认证控制器backend/controllers/authController.js):
// 简单登录逻辑,无密码验证
async function login(phone, password) {
try {
const [user] = await pool.execute('SELECT * FROM users WHERE phone = ?', [phone]);
if (!user.length) {
return { success: false, message: '登录失败' };
}
// 直接返回登录成功,未验证密码
return { success: true, data: { user: user[0], token: generateToken(user[0].id) } };
} catch (error) {
console.error('登录失败:', error);
return { success: false, message: '登录失败' };
}
}
修复后(用户模型backend/models/User.js):
const bcrypt = require('bcrypt');
const config = require('../config/config');
// 密码加密存储
async create(userData) {
try {
const id = uuidv4();
const now = new Date();
// 加密密码,盐值强度从配置文件读取
let hashedPassword = userData.password;
if (hashedPassword) {
hashedPassword = await bcrypt.hash(hashedPassword, config.bcrypt.saltRounds || 10);
}
const sql = `
INSERT INTO users (
id, phone, nickname, password, email, avatar, gender, age, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
id,
userData.phone,
userData.nickname,
hashedPassword,
userData.email || null,
userData.avatar || null,
userData.gender || 'unknown',
userData.age || null,
'active',
now,
now
];
await pool.execute(sql, params);
return { success: true, data: { id } };
} catch (error) {
console.error('创建用户失败:', error);
return { success: false, message: '创建用户失败', error: error.message };
}
}
// 密码验证实例方法
async validPassword(password) {
return await bcrypt.compare(password, this.password);
}
// 静态方法:根据手机号查找用户
static async findOne({ where }) {
const [rows] = await pool.execute('SELECT * FROM users WHERE phone = ?', [where.phone]);
if (rows.length) {
const user = rows[0];
// 绑定验证方法
user.validPassword = this.prototype.validPassword;
return user;
}
return null;
}
修复后(认证控制器backend/controllers/authController.js):
// 安全登录逻辑
async function login(phone, password) {
try {
// 查找用户
const user = await User.findOne({ where: { phone } });
if (!user) {
return { success: false, message: '用户不存在', code: 401 };
}
// 验证密码
const isPasswordValid = await user.validPassword(password);
if (!isPasswordValid) {
return { success: false, message: '密码错误', code: 401 };
}
// 更新最后登录时间
await pool.execute(
'UPDATE users SET last_login_at = ? WHERE id = ?',
[new Date(), user.id]
);
// 生成JWT令牌
const token = generateToken(user.id);
return { success: true, message: '登录成功', data: {
user: {
id: user.id,
phone: user.phone,
nickname: user.nickname,
status: user.status
},
token
} };
} catch (error) {
console.error('登录失败:', error);
return { success: false, message: '登录失败,请稍后重试', code: 500, error: error.message };
}
}
// 验证码发送频率限制
async function canSendNewVerifyCode(phone, type = 'register') {
try {
const [rows] = await pool.execute(`
SELECT * FROM verify_codes
WHERE phone = ? AND type = ? AND created_at > ?
ORDER BY created_at DESC
`, [phone, type, new Date(Date.now() - (config.verifyCode?.resendInterval || 60000))]);
return rows.length === 0;
} catch (error) {
console.error('检查验证码发送频率错误:', error);
return false;
}
}
| 对比项 | 修复前 | 修复后 |
|---|---|---|
| 密码存储 | 可能明文存储,风险高 | bcrypt加密存储,防泄露 |
| 密码验证 | 无验证流程 | 基于bcrypt.compare的安全验证 |
| 账户保护 | 无频率限制,易遭恶意攻击 | 验证码60秒内仅能发送一次 |
| 错误提示 | 模糊提示“登录失败” | 明确提示“用户不存在”/“密码错误” |
| 安全性 | 低 | 高(自适应哈希+频率限制+错误防护) |
核心问题:原数据库连接采用单次连接模式,频繁创建与销毁连接导致性能开销大,且缺乏快速验证连接状态的方法,开发调试时无法快速判断数据库可用性。
影响范围:
修复前:无连接池配置,单次连接模式(示例):
// 可能的单次连接方式
const mysql = require('mysql2');
async function query(sql, params) {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'poopcare'
});
return new Promise((resolve, reject) => {
connection.query(sql, params, (err, results) => {
connection.end();
if (err) reject(err);
else resolve(results);
});
});
}
修复后(backend/config/database.js):
const mysql = require('mysql2/promise');
const config = require('./config');
// 创建MySQL连接池
const pool = mysql.createPool({
host: config.database.host, // 主机地址(从配置文件读取)
port: config.database.port, // 端口号
user: config.database.username, // 用户名
password: config.database.password, // 密码
database: config.database.database, // 数据库名
charset: 'utf8mb4', // 支持emoji的字符集
collation: 'utf8mb4_unicode_ci', // 校对规则
waitForConnections: true, // 无可用连接时等待
connectionLimit: config.database.pool?.max || 5, // 最大连接数
queueLimit: 0, // 无连接请求队列限制
namedPlaceholders: true // 启用命名占位符(:param)
});
// 数据库连接测试方法
const testConnection = async () => {
try {
const connection = await pool.getConnection();
console.log('数据库连接测试成功!');
connection.release(); // 释放连接回连接池
return true;
} catch (error) {
console.error('数据库连接测试失败:', error);
return false;
}
};
// 导出连接池与测试方法
module.exports = { pool, testConnection };
| 对比项 | 修复前 | 修复后 |
|---|---|---|
| 连接管理 | 单次连接,频繁创建销毁 | 连接池管理,复用连接 |
| 性能开销 | 高(连接创建销毁耗时) | 低(连接复用,减少开销) |
| 连接参数 | 基础配置,可能缺失关键参数 | 完整配置(字符集、校对规则等) |
| 测试功能 | 无连接测试方法 | 提供testConnection,快速验证连接 |
| SQL可读性 | 仅支持?占位符 | 支持命名占位符,可读性更高 |

核心问题:项目维护过程中需要清理测试数据或冗余数据,但手动操作需逐表处理外键约束,步骤繁琐且易出错,缺乏批量、便捷的清空工具。
影响范围:
修复前:无专用工具,手动清空(示例SQL):
-- 手动清空需执行多个命令,且需处理外键
SET FOREIGN_KEY_CHECKS = 0;
DELETE FROM poop_records;
DELETE FROM drink_records;
DELETE FROM users;
SET FOREIGN_KEY_CHECKS = 1;
修复后(backend/clear-database.js):
const { pool } = require('./config/database');
const readline = require('readline');
const { argv } = require('process');
// 需清空数据的核心表列表(按依赖顺序排列)
const TABLES_TO_CLEAR = [
'poop_records',
'drink_records',
'feedbacks',
'verify_codes',
'users'
];
// 清空单表数据
async function clearTableData(tableName) {
try {
// 临时禁用外键约束,避免关联错误
await pool.execute(`SET FOREIGN_KEY_CHECKS = 0`);
// 优先使用TRUNCATE(效率更高),失败则使用DELETE
try {
await pool.execute(`TRUNCATE TABLE ${tableName}`);
console.log(`✓ 成功清空表 ${tableName} 的数据(TRUNCATE)`);
} catch (truncateError) {
await pool.execute(`DELETE FROM ${tableName}`);
console.log(`✓ 成功清空表 ${tableName} 的数据(DELETE)`);
}
// 重新启用外键约束
await pool.execute(`SET FOREIGN_KEY_CHECKS = 1`);
return true;
} catch (error) {
// 确保异常时仍启用外键约束
await pool.execute(`SET FOREIGN_KEY_CHECKS = 1`);
console.error(`✗ 清空表 ${tableName} 数据失败:`, error.message);
return false;
}
}
// 交互式界面
function createInteractiveInterface() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('\n⚠️ 警告:此操作将清空指定表的所有数据,且无法恢复!');
console.log(`支持清空的表:${TABLES_TO_CLEAR.join(', ')}\n`);
rl.question('请选择操作:\n1. 清空所有表\n2. 清空指定表\n输入选项(1/2):', async (choice) => {
if (choice === '1') {
console.log('\n开始清空所有表数据...');
for (const table of TABLES_TO_CLEAR) {
await clearTableData(table);
}
} else if (choice === '2') {
rl.question(`请输入要清空的表名(多个用逗号分隔,可选:${TABLES_TO_CLEAR.join(', ')}):`, async (tableNames) => {
const tables = tableNames.split(',').map(t => t.trim()).filter(t => TABLES_TO_CLEAR.includes(t));
if (tables.length === 0) {
console.log('✗ 未选择有效表名');
rl.close();
return;
}
console.log(`\n开始清空表 ${tables.join(', ')} 数据...`);
for (const table of tables) {
await clearTableData(table);
}
rl.close();
});
} else {
console.log('✗ 无效选项');
rl.close();
return;
}
rl.close();
process.exit(0);
});
}
// 执行入口
(async () => {
// 测试数据库连接
const isConnected = await pool.getConnection().then(conn => {
conn.release();
return true;
}).catch(() => false);
if (!isConnected) {
console.error('✗ 数据库连接失败,无法执行清空操作');
process.exit(1);
}
// 非交互式模式(通过命令行参数)
if (argv.length > 2) {
const cmd = argv[2];
if (cmd === 'all') {
console.log('开始清空所有表数据...');
for (const table of TABLES_TO_CLEAR) {
await clearTableData(table);
}
} else if (cmd === 'table' && argv.length > 3) {
const tableNames = argv[3].split(',');
const validTables = tableNames.filter(t => TABLES_TO_CLEAR.includes(t));
for (const table of validTables) {
await clearTableData(table);
}
} else {
console.log('无效命令参数');
console.log('用法:');
console.log(' 清空所有表:node clear-database.js all');
console.log(' 清空指定表:node clear-database.js table 表名1,表名2');
}
process.exit(0);
}
// 交互式模式
createInteractiveInterface();
})();
| 对比项 | 修复前 | 修复后 |
|---|---|---|
| 操作方式 | 手动执行SQL命令 | 命令行工具(交互式/非交互式) |
| 外键处理 | 需手动禁用/启用 | 自动处理外键约束 |
| 操作效率 | 逐表执行,效率低 | 批量处理,支持全量/指定表清空 |
| 安全性 | 易误操作,无提示 | 警告提示,仅支持指定表清空 |
| 复用性 | 无复用性,重复操作 | 一次开发,多次使用,支持脚本调用 |
我们通过命令行测试、接口调试工具等方式验证了所有优化功能的正确性,测试结果如下:
=== 开始功能测试 ===
1. API文档路由测试...
测试命令:Invoke-WebRequest -Uri http://localhost:3000/api/docs -Method GET
响应结果:StatusCode: 200, StatusDescription: OK
测试结论:API文档路由公开访问正常,可视化页面可正常查看与调试
2. 前后端路由匹配测试...
前端请求URL:http://localhost:3000/api/health-record
提交数据:{ "typeIndex": 2, "moodIndex": 1, "time": "2025-12-20T10:00:00Z" }
响应结果:{ "success": true, "record": { "id": 3, "user_id": 3, "type_index": 2, ... } }
测试结论:路由匹配正常,健康记录提交成功
3. 健康记录列表获取测试...
请求URL:http://localhost:3000/api/health-record?page=1&limit=10
响应结果:{ "success": true, "data": [ { "id": 3, "time": "2025-12-20T10:00:00Z", ... } ] }
测试结论:getHealthRecords函数参数匹配正常,查询成功
4. 登录功能安全测试...
注册数据:{ "phone": "13800138001", "password": "Test123456", "nickname": "test_user2" }
登录请求(正确密码):{ "success": true, "message": "登录成功", "data": { "user": {...}, "token": "..." } }
登录请求(错误密码):{ "success": false, "message": "密码错误", "code": 401 }
验证码限流测试:60秒内重复发送,返回"无法重复发送"提示
测试结论:密码加密存储与验证功能正常,账户保护机制生效
5. 数据库连接测试...
执行命令:node config/database.js
输出结果:数据库连接测试成功!
测试结论:连接池配置正常,数据库连接可用
6. 数据库清空工具测试...
执行命令:node clear-database.js
交互操作:选择"清空所有表"
输出结果:
✓ 成功清空表 poop_records 的数据(TRUNCATE)
✓ 成功清空表 drink_records 的数据(TRUNCATE)
✓ 成功清空表 feedbacks 的数据(TRUNCATE)
✓ 成功清空表 verify_codes 的数据(TRUNCATE)
✓ 成功清空表 users 的数据(TRUNCATE)
测试结论:工具可正常清空数据,外键约束处理正常
=== 测试完成 ===
并且我们也通过前端HBuilderX录入数据:

在成功连接的数据库中,我们也可以看到刚刚手动录入的两次数据,证明切实可用

测试结果显示: