# PRD 功能缺口精确分析与完善规划设计

**文档版本**: v1.0  
**编写日期**: 2026-04-01  
**基于**: CODE_REVIEW_REPORT_2026-04-01-V2.md + 实际代码逐行核查  
**目的**: 纠正历史报告的模糊描述，提供可执行的实现规划

---

## 一、核查方法与结论修正

本次对七项"已知缺口"进行了**逐文件逐行**的实际代码核查，结论如下：

| 缺口编号 | 历史报告结论 | 本次核查实际结论 | 变更 |
|---------|-------------|----------------|------|
| GAP-01 | 角色继承递归查询未实现 | ⚠️ **部分实现** — 逻辑层完整，但启动时未接入 | ↑ 修正 |
| GAP-02 | 密码重置（手机短信）未实现 | ✅ **已完整实现** — Service + Handler + 路由全部到位 | ✅ 关闭 |
| GAP-03 | 设备信任功能未实现 | ⚠️ **部分实现** — CRUD 完整，但登录流程未接入信任检查 | ↑ 修正 |
| GAP-04 | SSO（CAS/SAML）未实现 | ❌ **确认未实现** — SSOManager 是 OAuth2 包装，无 CAS/SAML | 维持 |
| GAP-05 | 异地登录检测未实现 | ⚠️ **部分实现** — AnomalyDetector 已有检测逻辑，但未接入启动流程 | ↑ 修正 |
| GAP-06 | 异常设备检测未实现 | ⚠️ **部分实现** — AnomalyDetector 有 NewDevice 事件，但设备指纹未采集 | ↑ 修正 |
| GAP-07 | SDK 支持未实现 | ❌ **确认未实现** — 无任何 SDK 文件 | 维持 |

**重新分类后**：
- ✅ 已完整实现（可关闭）：1 项（GAP-02）
- ⚠️ 骨架已有、接线缺失（低成本完成）：3 项（GAP-01、GAP-03、GAP-05/06）
- ❌ 需从零构建（高成本）：2 项（GAP-04 SSO、GAP-07 SDK）

---

## 二、各缺口精确诊断

---

### GAP-01：角色继承递归查询

#### 现状核查

**已实现的部分（代码证据）：**

```go
// internal/repository/role.go:178-213
// GetAncestorIDs 获取角色的所有祖先角色ID
func (r *RoleRepository) GetAncestorIDs(ctx context.Context, roleID int64) ([]int64, error) {
    // 循环向上查找父角色，直到没有父角色为止 ✅
}

// GetAncestors — 完整继承链 ✅

// internal/service/role.go:191-213
// GetRolePermissions — 已调用 GetAncestorIDs，合并所有祖先权限 ✅
```

**缺失的部分：**

1. **循环引用检测缺失**：`UpdateRole` 允许修改 `parent_id`，但不检测循环：A 的父是 B，B 的父又改成 A → 死循环
2. **深度限制缺失**：PRD 要求"继承深度可配置"，代码无上限保护
3. **用户权限查询未走继承路径**：
   - `authMiddleware` 中校验用户权限时，直接查 `user_role_permissions`，未调用 `GetRolePermissions`
   - 实际登录时 JWT 中的 permissions 也未包含继承权限

```go
// cmd/server/main.go — 完全没有以下调用：
// authService.SetAnomalyDetector(...)  ← 未接入
// 角色继承在 auth middleware 中也未走 GetRolePermissions
```

#### 问题等级
🟡 **中危** — 角色继承数据结构完整，但运行时不生效，是"假继承"

---

### GAP-02：密码重置（手机短信）

#### 现状核查

**完整实现证据：**

```
internal/service/password_reset.go
  - ForgotPasswordByPhone()     ✅ 生成6位验证码，缓存用户ID
  - ResetPasswordByPhone()      ✅ 验证码校验 + 密码重置

internal/api/handler/password_reset_handler.go
  - ForgotPasswordByPhone()     ✅ Handler 完整
  - ResetPasswordByPhone()      ✅ Handler 完整

internal/api/router/router.go:138-139
  - POST /api/v1/auth/forgot-password/phone  ✅ 路由已注册
  - POST /api/v1/auth/reset-password/phone   ✅ 路由已注册
```

**遗留问题（不影响功能闭合，但有质量风险）：**

```go
// password_reset_handler.go:100-101
// 获取验证码（不发送，由调用方通过其他渠道发送）
code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone)
// 问题：code 被返回给 HTTP 调用方（可能是接口直接返回了明文验证码）
```

需确认 handler 是否把 code 暴露在响应体中。

#### 结论
✅ **此条缺口可关闭**，但需确认验证码不在响应中明文返回。

---

### GAP-03：设备信任功能

#### 现状核查

**已实现的部分：**

```
internal/domain/device.go
  - Device 模型：IsTrusted、TrustExpiresAt 字段 ✅

internal/repository/device.go
  - TrustDevice() / UntrustDevice()    ✅
  - GetTrustedDevices()               ✅

internal/service/device.go
  - TrustDevice(ctx, deviceID, trustDuration)  ✅
  - UntrustDevice()                   ✅
  - GetTrustedDevices()               ✅

internal/api/handler/device_handler.go
  - TrustDevice Handler               ✅
  - UntrustDevice Handler             ✅
  - GetMyTrustedDevices Handler       ✅

internal/api/router/router.go
  - POST /api/v1/devices/:id/trust     ✅
  - DELETE /api/v1/devices/:id/trust   ✅
  - GET /api/v1/devices/me/trusted     ✅
```

**缺失的关键接线：**

1. **登录流程未检查设备信任**：登录时没有"设备是否已信任 → 跳过 2FA"的逻辑
2. **登录请求无设备指纹字段**：`LoginRequest` 中无 `device_id` 或 `device_fingerprint`
3. **注册/登录后未自动创建 Device 记录**：用户登录后设备不会自动登记
4. **信任期限过期检查仅在查询时**：没有后台清理过期信任设备的 goroutine（虽然查询已过滤，但数据库垃圾数据会积累）
5. **前端无设备管理页面**：无法让用户查看/管理已登录设备

#### 问题等级
🟡 **中危** — API 骨架完整，但核心场景（信任设备免二次验证）未接线

---

### GAP-04：SSO（CAS/SAML 协议）

#### 现状核查

```go
// internal/auth/sso.go（SSOManager）
// 实现了 OAuth2 客户端模式的单点登录
// 支持：GitHub、Google 等 OAuth2 提供商的 SSO 接入

// 不支持的协议：
// - CAS (Central Authentication Service)：无任何实现
// - SAML 2.0：无任何实现
```

PRD 3.3 要求："支持 CAS、SAML 协议（**可选**）"

#### 分析
PRD 明确标注"可选"，CAS/SAML 是企业级 IdP（如 Okta、Active Directory）集成所需。
实现成本：**每个协议 ≥ 2 周**，属于大型独立特性。

#### 问题等级
💭 **低优先级** — PRD 标注可选，且 OAuth2 SSO 已实现；建议推迟到 v2.0

---

### GAP-05：异地登录检测

#### 现状核查

**已实现的部分：**

```go
// internal/security/ip_filter.go:182-359
// AnomalyDetector 完整实现：
//   - AnomalyNewLocation：新地区登录检测 ✅
//   - AnomalyBruteForce：暴力破解检测 ✅  
//   - AnomalyMultipleIP：多IP检测 ✅
//   - AnomalyNewDevice：新设备检测 ✅
//   - 自动封禁 IP ✅

// internal/service/auth.go:62-64
// anomalyRecorder 接口已定义 ✅

// internal/service/auth.go:199-201
// SetAnomalyDetector(detector anomalyRecorder) ✅ 方法存在
```

**关键缺口：**

```go
// cmd/server/main.go — 完全没有这两行：
anomalyDetector := security.NewAnomalyDetector(...)
authService.SetAnomalyDetector(anomalyDetector)
// 结果：anomalyDetector == nil，所有检测静默跳过
```

```go
// internal/service/auth.go:659-660（登录时传入的地理位置）
s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", false)
// location 和 deviceFingerprint 都是空字符串！
// 即使接入了 AnomalyDetector，新地区检测也无法工作
```

**根本原因**：缺少 IP 地理位置解析模块（需要 MaxMind GeoIP 或类似数据库）

#### 问题等级
🟡 **中危** — 检测引擎已有，但需要两步接线：① 启动时注入 ② 登录时传入真实地理位置

---

### GAP-06：异常设备检测

#### 现状核查

**已实现：**
- `AnomalyDetector.detect()` 中的 `AnomalyNewDevice` 事件检测逻辑 ✅
- `Device` domain 模型完整 ✅

**缺失：**
1. **前端无设备指纹采集**：登录请求中无 `device_fingerprint` 字段
2. **后端 Login 接口不接收指纹**：`LoginRequest` 中无此字段
3. **即使有指纹，检测器未注入**（同 GAP-05）

#### 与 GAP-05 的关系
GAP-05（异地登录）和 GAP-06（异常设备）共享同一套 `AnomalyDetector` 基础设施，**同一批工作可以一起完成**。

---

### GAP-07：SDK 支持（Java/Go/Rust）

#### 现状核查
无任何 SDK 代码或目录结构。

#### 分析
SDK 本质上是对 RESTful API 的客户端包装，而当前 API 文档（Swagger）已完整。

**优先级**：每个 SDK 工作量 ≥ 2 周，且需独立仓库、版本管理、CI 发布；属于产品生态建设，与当前版本核心功能无关。

#### 问题等级
💭 **低优先级** — 建议 v2.0 后根据实际用户需求再决定

---

## 三、密码历史记录（新发现缺口）

### 现状核查

```
internal/repository/password_history.go — Repository 完整实现 ✅
internal/domain/  — PasswordHistory 模型存在（需确认）
```

**缺失：**
```go
// cmd/server/main.go — 无以下初始化：
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
// authService 中也无 "修改密码时检查历史记录" 的逻辑
```

PRD 1.4 要求："密码历史记录（防止重复使用）"

**等级**：🟡 建议级 — Repository 已有，service 层接线缺失

---

## 四、完善规划设计

### 4.1 优先级矩阵

| 缺口 | 优先级 | 工作量 | 依赖 | 建议迭代 |
|------|--------|--------|------|---------|
| GAP-01 角色继承接线 + 循环检测 | P1 🔴 | S（2天）| 无 | 当前迭代 |
| GAP-03 设备信任接线（登录检查）| P1 🔴 | M（4天）| 前端配合 | 当前迭代 |
| GAP-05/06 异常检测接线 | P2 🟡 | M（5天）| IP 地理库 | 下一迭代 |
| 密码历史记录（新发现）| P2 🟡 | S（1天）| 无 | 当前迭代 |
| GAP-02 验证码安全确认 | P1 🔴 | XS（0.5天）| 无 | 当前迭代 |
| GAP-04 CAS/SAML | P4 | L（2周+）| 无 | v2.0 |
| GAP-07 SDK | P5 | L（2周+/SDK）| API 稳定 | v2.0 |

---

### 4.2 GAP-01：角色继承 — 完整规划

#### 问题根因
角色继承的 Repository/Service 层已完整，但：
1. `authMiddleware` 权限校验未使用 `GetRolePermissions`（含继承）
2. `UpdateRole` 无环形继承检测
3. 无继承深度上限

#### 实现方案

**Step 1：修复 UpdateRole 循环检测（`internal/service/role.go`）**

```go
func (s *RoleService) UpdateRole(ctx context.Context, roleID int64, req *UpdateRoleRequest) (*domain.Role, error) {
    // ... 现有逻辑 ...
    if req.ParentID != nil {
        if *req.ParentID == roleID {
            return nil, errors.New("不能将角色设置为自己的父角色")
        }
        // 新增：检测循环引用
        if err := s.checkCircularInheritance(ctx, roleID, *req.ParentID); err != nil {
            return nil, err
        }
        // 新增：检测深度
        if err := s.checkInheritanceDepth(ctx, *req.ParentID, maxRoleDepth); err != nil {
            return nil, err
        }
    }
}

const maxRoleDepth = 5 // 可配置

func (s *RoleService) checkCircularInheritance(ctx context.Context, roleID, newParentID int64) error {
    // 向上遍历 newParentID 的祖先链，检查 roleID 是否出现
    ancestors, err := s.roleRepo.GetAncestorIDs(ctx, newParentID)
    if err != nil {
        return err
    }
    for _, id := range ancestors {
        if id == roleID {
            return errors.New("检测到循环继承，操作被拒绝")
        }
    }
    return nil
}
```

**Step 2：auth middleware 使用继承权限（`internal/api/middleware/auth.go`）**

```go
// 修改 getUserPermissions 方法
// 当前：直接查 role_permissions 表
// 目标：调用 roleService.GetRolePermissions(ctx, roleID)（含继承）
// 注意：需要把 roleService 注入到 authMiddleware，或在 rolePermissionRepo 层实现
```

**Step 3：JWT 生成时包含继承权限**

当用户登录后生成 JWT，在 `generateLoginResponse` 中调用 `GetRolePermissions` 替代直接查询：

```go
// internal/service/auth.go:generateLoginResponse
// 现状：permissions 只来自直接绑定的权限
// 目标：permissions = 直接权限 ∪ 所有祖先角色的权限
```

#### 测试用例设计
```
1. 创建角色 A（根）→ 角色 B（parent=A）→ 角色 C（parent=B）
2. 给角色 A 分配权限 P1，给角色 B 分配 P2
3. 用户分配角色 C → 应能访问 P1、P2、以及 C 自身权限
4. 尝试设置角色 A 的 parent 为 C → 应报错"循环继承"
5. 创建深度 > maxRoleDepth 的继承链 → 应报错
```

---

### 4.3 GAP-02：密码短信重置 — 安全确认

#### 需确认的问题

```go
// internal/api/handler/password_reset_handler.go:100-124
code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone)
// 需要检查：code 是否被写入了 HTTP 响应体
```

**预期正确行为**：
- code 生成后，应通过 SMS 服务发送到用户手机（或 `h.smsService.Send(phone, code)`）
- HTTP 响应仅返回 `{"message": "verification code sent"}`，不返回 code 明文

**如果当前实现了直接返回 code**：这是 🔴 安全漏洞，必须修复。

#### 修复方案
```go
func (h *PasswordResetHandler) ForgotPasswordByPhone(c *gin.Context) {
    // ...
    code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone)
    if err != nil {
        handleError(c, err)
        return
    }
    
    // 通过 SMS 服务发送验证码（不在响应中返回）
    if h.smsService != nil {
        if err := h.smsService.SendCode(req.Phone, code); err != nil {
            // fail-closed：SMS 发送失败应报错，不假装成功
            c.JSON(http.StatusServiceUnavailable, gin.H{"error": "验证码发送失败，请稍后重试"})
            return
        }
    }
    
    // 响应不包含 code
    c.JSON(http.StatusOK, gin.H{"message": "verification code sent"})
}
```

---

### 4.4 GAP-03：设备信任接线 — 完整规划

#### 实现方案

**Step 1：登录请求接收设备标识**

```go
// internal/service/auth.go
type LoginRequest struct {
    Account      string `json:"account"`
    Password     string `json:"password"`
    Remember     bool   `json:"remember"`
    DeviceID     string `json:"device_id,omitempty"`      // 新增
    DeviceName   string `json:"device_name,omitempty"`    // 新增
    DeviceBrowser string `json:"device_browser,omitempty"` // 新增
    DeviceOS     string `json:"device_os,omitempty"`      // 新增
}
```

**Step 2：登录时自动记录设备**

```go
// internal/service/auth.go:generateLoginResponse 中增加设备记录
func (s *AuthService) generateLoginResponse(ctx context.Context, user *domain.User, req *LoginRequest) (*LoginResponse, error) {
    // ... token 生成 ...
    
    // 自动注册/更新设备记录
    if s.deviceRepo != nil && req.DeviceID != "" {
        s.bestEffortRegisterDevice(ctx, user.ID, req)
    }
    
    // ... 返回 ...
}
```

**Step 3：TOTP 验证时检查设备信任**

```go
// internal/service/auth.go — 2FA 验证流程中
func (s *AuthService) VerifyTOTP(ctx context.Context, ..., deviceID string) error {
    // 检查设备是否已信任
    if deviceID != "" && s.deviceRepo != nil {
        device, err := s.deviceRepo.GetByDeviceID(ctx, userID, deviceID)
        if err == nil && device.IsTrusted {
            // 检查信任是否过期
            if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) {
                return nil // 跳过 2FA
            }
        }
    }
    // 正常 TOTP 验证流程
}
```

**Step 4："记住此设备"信任接口**

已有 `POST /devices/:id/trust`，但需要前端在 2FA 验证通过时提供"记住此设备"选项并调用该接口。

**前端工作（ProfileSecurityPage 或登录流程）**：
- 登录时在设备指纹字段传入 `navigator.userAgent + screen.width + timezone` 的 hash
- 2FA 验证界面添加"记住此设备（30天）"复选框
- 勾选后调用 `POST /devices/:id/trust { trust_duration: "30d" }`

---

### 4.5 GAP-05/06：异常登录检测接线 — 完整规划

#### 方案A：纯内存检测（无 GeoIP，当前可立即实现）

只做 IP/设备维度的检测，不依赖地理位置：

```go
// cmd/server/main.go — 加入以下代码
anomalyDetector := security.NewAnomalyDetector(security.AnomalyDetectorConfig{
    WindowSize:          24 * time.Hour,
    MaxFailures:         10,
    MaxIPs:              5,
    MaxRecords:          100,
    AutoBlockDuration:   30 * time.Minute,
    KnownLocationsLimit: 3,
    KnownDevicesLimit:   5,
    IPFilter:            ipFilter, // 复用现有 ipFilter
})
authService.SetAnomalyDetector(anomalyDetector)
```

登录时传入真实设备指纹（从 User-Agent 等提取）：

```go
// internal/service/auth.go:Login()
deviceFingerprint := extractDeviceFingerprint(req.UserAgent, req.DeviceID)
s.recordLoginAnomaly(ctx, &user.ID, ip, "", deviceFingerprint, true)
// （location 暂为空，等 GeoIP 接入后再填）
```

#### 方案B：接入 GeoIP（可选，v1.1 引入）

```go
// 使用 MaxMind GeoLite2（免费）或 ip-api.com（HTTP 方式）
// 在登录时：
location := geoip.Lookup(ip) // → "广东省广州市" or "US/California"
s.recordLoginAnomaly(ctx, &user.ID, ip, location, deviceFingerprint, true)
```

**建议**：方案A 立即实现（工作量约 1 天），方案B 作为可选增强。

#### 异常事件通知
`AnomalyDetector` 检测到异常后，当前只记录日志（通过 `publishEvent`）。  
需补充：
- 邮件通知用户（利用现有 `auth_email.go` 的邮件发送能力）
- 写入 OperationLog 或专门的 SecurityAlert 表

---

### 4.6 密码历史记录（新发现缺口）— 规划

#### 工作量
极小，所有基础设施已就绪。

#### 实现步骤

**Step 1：`cmd/server/main.go` 初始化**
```go
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
authService.SetPasswordHistoryRepository(passwordHistoryRepo)
```

**Step 2：AuthService 接收依赖**
```go
type AuthService struct {
    // ...
    passwordHistoryRepo passwordHistoryRepositoryInterface // 新增
}
```

**Step 3：修改密码时检查历史**
```go
func (s *AuthService) ChangePassword(ctx context.Context, userID int64, newPassword string) error {
    // ... 验证新密码强度 ...
    
    // 检查密码历史（默认保留最近5个）
    if s.passwordHistoryRepo != nil {
        histories, _ := s.passwordHistoryRepo.GetByUserID(ctx, userID, 5)
        for _, h := range histories {
            if auth.VerifyPassword(h.PasswordHash, newPassword) {
                return errors.New("新密码不能与最近5次密码相同")
            }
        }
    }
    
    // 保存新密码哈希到历史
    go func() {
        _ = s.passwordHistoryRepo.Create(ctx, &domain.PasswordHistory{
            UserID:       userID,
            PasswordHash: newHashedPassword,
        })
        _ = s.passwordHistoryRepo.DeleteOldRecords(ctx, userID, 5)
    }()
}
```

---

## 五、实现时序建议

### Sprint 1（当前迭代，约 1 周）

| 任务 | 负责层 | 工作量 |
|------|--------|--------|
| GAP-02 验证码安全确认 + fix | 后端 handler | 0.5d |
| 密码历史记录接线 | 后端 service | 1d |
| GAP-01 循环继承检测 | 后端 service | 1d |
| GAP-05 方案A：AnomalyDetector 接入启动流程 | 后端 main.go | 1d |
| GAP-01 auth middleware 使用继承权限 | 后端 middleware | 1.5d |

### Sprint 2（下一迭代，约 2 周）

| 任务 | 负责层 | 工作量 |
|------|--------|--------|
| GAP-03 登录接收设备指纹 | 后端 service + 前端 | 2d |
| GAP-03 2FA 信任设备免验证 | 后端 service | 1d |
| GAP-03 前端设备管理页面 | 前端 | 3d |
| GAP-05/06 设备指纹采集 + 新设备通知 | 前端 + 后端 | 2d |

### v2.0 规划（暂不排期）

| 任务 | 说明 |
|------|------|
| GAP-04 CAS 协议 | 需引入 `gosaml2` 或 `cas` 库 |
| GAP-04 SAML 2.0 | 需引入 `saml` 相关库 |
| GAP-07 Go SDK | 基于已有 API 生成 SDK，独立仓库 |
| GAP-07 Java SDK | 独立仓库，Maven/Gradle |
| GAP-05 GeoIP 接入 | MaxMind GeoLite2 或 ip-api.com |

---

## 六、验收标准

每个 Gap 修复完成后，必须满足以下验收条件：

### GAP-01 角色继承
- [ ] 单元测试：用户持有子角色，能访问父角色绑定的权限
- [ ] 单元测试：设置循环继承返回 `errors.New("循环继承")`
- [ ] 手动验证：深度 > 5 的继承被拒绝
- [ ] `go test ./...` 全通过

### GAP-02 密码短信重置
- [ ] 代码确认响应体中无明文验证码
- [ ] 单元测试：错误验证码返回 401
- [ ] 单元测试：验证码过期后返回失败

### GAP-03 设备信任
- [ ] 登录接口能接收 `device_id`
- [ ] 登录后 `/devices` 列表出现新设备记录
- [ ] 信任设备后，2FA 验证被跳过
- [ ] 信任过期后，重新要求 2FA

### GAP-05/06 异常检测
- [ ] 启动日志出现 "anomaly detector initialized"
- [ ] 10次失败登录触发 `AnomalyBruteForce` 事件
- [ ] 事件写入 operation_log 或日志可查
- [ ] `go test ./...` 全通过

### 密码历史记录
- [ ] 修改密码时，使用历史密码被拒绝
- [ ] 历史记录不超过 5 条（旧的被清理）

---

## 七、文件变更清单（预计）

### 后端变更文件

| 文件 | 变更类型 | Gap |
|------|---------|-----|
| `cmd/server/main.go` | 修改：注入 anomalyDetector、passwordHistoryRepo | GAP-05、密码历史 |
| `internal/service/role.go` | 修改：增加循环检测和深度检测 | GAP-01 |
| `internal/service/auth.go` | 修改：generateLoginResponse 含继承权限；登录时传设备指纹 | GAP-01、GAP-03、GAP-05 |
| `internal/api/middleware/auth.go` | 修改：权限校验走继承路径 | GAP-01 |
| `internal/api/handler/password_reset_handler.go` | 修改：确认不返回明文 code | GAP-02 |

### 前端变更文件（Sprint 2）

| 文件 | 变更类型 | Gap |
|------|---------|-----|
| `src/pages/auth/LoginPage.tsx` | 修改：登录时采集设备指纹 | GAP-03、GAP-06 |
| `src/pages/profile/ProfileSecurityPage.tsx` | 修改：2FA 验证加"记住设备"选项 | GAP-03 |
| `src/pages/admin/DevicesPage.tsx` | 新增：设备管理页面 | GAP-03 |

---

*本文档由代码审查专家 Agent 生成，2026-04-01*  
*基于实际代码逐行核查，历史报告中的模糊描述已全部纠正*
