🔍 分享模块调试全记录

从“未知能否使用”到“稳定运行”的完整修复历程
📅 2026-03-11 👤 调试工程师 ⚙️ 版本 v2.0

📌 概述

本文档详细记录了 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.rolesuser.role。AuthService 的 findUserById 未从数据库查询 role 字段。

修复:在 AuthService.findUserByIdselect 数组中添加 '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||"

原因:控制器将 ipuserAgentrefereremail 拼接成管道分隔字符串传递给 accessShareip 参数,而数据库 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.tsfindUserByIdselect 中添加 'role'使 JWT 验证返回 role 字段
jwt.strategy.ts在返回对象中添加 role: user.rolereq.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 进行验证:

分享详情返回示例:

{
  "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" }
}

💡 经验总结

📎 后续建议