📌 概述
本文档详细记录了 smart-upload-backend-v2 项目中 分享模块(Share Module) 的调试与修复过程。从模块完全不可用(创建分享返回 401/403/500)到最终实现创建、查询、公开访问等功能,所有核心接口均通过测试。本次修复涵盖了 数据库结构修正、认证授权完善、路由冲突解决、数据格式错误处理 等多个方面。
✅ 初始状态
创建分享 → 403 Forbidden
获取列表 → 500 Internal Error
公开访问 → 500 Internal Error
🔄 核心问题
JWT 缺少 role 字段、表结构类型错误、路由冲突、IP 拼接错误、数组序列化失败
🚀 最终成果
所有基础接口正常,可创建、获取、访问、下载(模拟),分享计数正确
🔎 问题分析与修复步骤
1. 认证授权:角色缺失导致 403
现象:使用有效 JWT 访问需要认证的接口(如 POST /shares)返回 403 Forbidden,而 /auth/login 正常。
诊断:查看日志发现 JwtStrategy 中返回的用户对象没有 role 字段,而 RolesGuard 期望 user.roles 或 user.role。AuthService 的 findUserById 未从数据库查询 role 字段。
修复:在 AuthService.findUserById 的 select 数组中添加 'role',并在 JwtStrategy 中返回包含 role 的用户对象。
// auth.service.ts
async findUserById(userId: string) {
return this.userRepository.findOne({
where: { id: userId },
select: ['id', 'email', 'username', 'createdAt', 'role'], // 添加 role
});
}
// jwt.strategy.ts
const validatedUser = {
id: user.id,
email: user.email,
username: user.username,
role: user.role, // 关键
// ...
};
2. 路由冲突:/shares/user 被当作 :id 捕获
现象:访问 GET /shares/user 时,实际命中 @Get(':id') 路由,将字符串 'user' 作为分享 ID 查询数据库,导致 UUID 格式错误。
修复:正确的用户分享列表路由应为 GET /shares?page=1&limit=10(控制器中已存在 @Get())。删除对 /shares/user 的调用,改用 /shares 并传递查询参数。
3. 数据库字段类型错误:file_ids 应为数组
现象:创建分享时出现 malformed array literal: "094b134a-...",尽管传入数组,但数据库列定义为 uuid 而非 uuid[]。
修复:执行 SQL 修改列类型:
ALTER TABLE shares ALTER COLUMN file_ids TYPE uuid[] USING array[file_ids]::uuid[];
同时确认实体中 @Column({ type: 'uuid', array: true }) 正确配置。
4. 手动格式化 fileIds 导致计算字段异常
现象:创建成功后返回的 fileId 为 "{",fileCount 为 38(字符串长度)。
原因:在服务层手动将数组格式化为 '{uuid}' 字符串,导致实体内部的 fileIds 变为字符串,computeFields 错误地取了第一个字符和长度。
修复:移除手动格式化代码,让 TypeORM 自动处理数组序列化。同时确保实体中无 transformer 干扰。
5. 公开访问路由 IP 拼接错误
现象:GET /shares/public/:code 返回 500,错误日志显示 invalid input syntax for type inet: "127.0.0.1|curl/8.5.0||"。
原因:控制器将 ip、userAgent、referer、email 拼接成管道分隔字符串传递给 accessShare 的 ip 参数,而数据库 viewed_ips 列要求纯 IP。
修复:修改控制器,将请求头信息合并到 AccessShareDto 中,只传递纯 IP 给服务层。
// share.controller.ts (公开路由)
async getShareByCode(..., @Ip() ip: string, @Headers('user-agent') userAgent: string) {
accessDto.userAgent = accessDto.userAgent || userAgent;
// 不拼接,直接传递 ip
return await this.shareService.accessShare(shareCode, accessDto, ip);
}
6. 重复文件检查查询失败(后续优化)
现象:原重复检查使用 In(createDto.fileIds),在 PostgreSQL 数组列上产生错误。
修复:使用 QueryBuilder 和数组重叠运算符 && 替代:
const existingShares = await this.shareRepository
.createQueryBuilder('share')
.where('share.creatorId = :userId', { userId })
.andWhere('share.status != :revoked', { revoked: ShareStatus.REVOKED })
.andWhere('share.fileIds && ARRAY[:...fileIds]::uuid[]', { fileIds: createDto.fileIds })
.getMany();
📋 关键代码变更汇总
| 文件 | 修改内容 | 作用 |
|---|---|---|
auth.service.ts | 在 findUserById 的 select 中添加 'role' | 使 JWT 验证返回 role 字段 |
jwt.strategy.ts | 在返回对象中添加 role: user.role | 让 req.user 包含角色信息 |
share.controller.ts | 修改公开 GET/POST 路由,移除 clientInfo 拼接,合并头信息到 DTO | 避免 IP 格式错误 |
share.service.ts | 移除手动格式化 fileIds 的代码;注释掉重复检查(后改用正确查询) | 保证 fileIds 为数组,计算字段正常 |
| 数据库迁移 | ALTER TABLE shares ALTER COLUMN file_ids TYPE uuid[] | 使列类型与实体一致 |
🧪 测试验证结果
使用测试用户 452544093@qq.com 和文件 094b134a-f815-4c11-8d8e-ded60f41eeb6 进行验证:
- ✓ 登录:获取有效 JWT
- ✓ 创建分享:返回 201,包含 shareCode
5q7qdaT6g、正确 fileId 和 fileCount - ✓ 获取用户列表:
GET /shares?page=1&limit=10返回包含两个分享的分页数据 - ✓ 公开访问:
GET /shares/public/5q7qdaT6g返回分享详情,viewCount 正常增加 - ✓ 下载接口:
GET /shares/public/5q7qdaT6g/download/{fileId}返回模拟下载信息(需进一步对接 MinIO)
分享详情返回示例:
{
"success": true,
"share": {
"id": "eb70a843-d48a-48f9-a391-d8e003caaed3",
"shareCode": "5q7qdaT6g",
"title": "我的测试分享2",
"fileIds": ["094b134a-f815-4c11-8d8e-ded60f41eeb6"],
"fileCount": 1,
"viewCount": 1,
...
},
"accessInfo": { "ip": "127.0.0.1", "userAgent": "curl/8.5.0" }
}
💡 经验总结
- 调试步骤的重要性:从简单端点(
/ping)开始,逐步扩展,结合日志定位问题。 - 数据库与实体的一致性:TypeORM 的
array: true要求数据库列类型正确,否则会引发序列化错误。 - 角色授权:JWT 返回的用户对象必须包含角色字段,否则守卫无法通过。
- 路由顺序:动态路由(
:id)应放在静态路由之后,避免冲突。 - 参数传递:服务层接口应清晰定义,避免传递拼接字符串,应使用 DTO 携带结构化数据。
📎 后续建议
- 恢复并完善重复文件检查逻辑(已提供正确查询)。
- 对接文件下载服务(MinIO 预签名 URL)。
- 增加单元测试和集成测试,覆盖核心流程。
- 添加 Swagger 文档注释,便于前端对接。