## Context

当前项目处于框架搭建阶段，存在多处技术债务需要清理：
- 两套 Auth 实现产生于不同开发阶段，未整合
- 示例代码（user/order）是早期测试用途，现已有真实 RBAC 代码
- pkg/errors 和 pkg/response 设计时职责划分不清晰
- DataPermissionScope 实现完整但从未集成使用

**约束条件**：
- 必须保持 Go 惯用模式，避免 Java 风格过度抽象
- main.go 在未来开发中应该不需要修改
- 数据权限过滤必须支持绕过机制

## Goals / Non-Goals

### Goals
- 清理所有示例和重复代码，使框架干净整洁
- 统一认证、错误处理、响应格式的实现方式
- 实现数据权限的 GORM 自动化过滤
- 将组件初始化从 main.go 解耦，支持未来扩展
- 在关键扩展点添加 TODO 标记

### Non-Goals
- 不实现完整的 DI 框架（保持 Go 简洁风格）
- 不实现自动注册机制（使用显式工厂模式）
- 不重构现有 RBAC 业务逻辑
- 不添加新的业务功能

## Decisions

### Decision 1: Auth 中间件合并策略

**选择**：重新设计合并版本

**实现方案**：
```go
// pkg/middleware/auth.go - 合并版本
type AuthConfig struct {
    TokenExtractor func(*fiber.Ctx) string    // 自定义 token 提取
    SkipPaths      []string                   // 跳过认证的路径
    Validator      func(string) (*UserInfo, error) // token 验证函数
}

func Auth(cfg AuthConfig) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // 1. 检查跳过路径
        // 2. 提取 token
        // 3. 验证 token
        // 4. 设置用户上下文（同时设置 Locals 和 Context）
        // 5. 错误统一返回 AppError，由全局 ErrorHandler 处理
    }
}
```

**理由**：
- 合并两者优点：pkg 版本的可配置性 + internal 版本的统一错误格式
- 统一使用 `return errors.New()` 让全局 ErrorHandler 处理
- 消除错误格式不一致问题

### Decision 2: 组件注册解耦策略

**选择**：Bootstrap 包 + 按模块拆分 + 工厂函数模式

**实现方案**：
```
internal/bootstrap/
├── bootstrap.go      # 主入口，编排初始化流程
├── dependencies.go   # Dependencies 结构体定义
├── stores.go         # 所有 Store 初始化逻辑
├── services.go       # 所有 Service 初始化逻辑
├── handlers.go       # 所有 Handler 初始化逻辑
└── types.go          # Handlers 结构体定义
```

**bootstrap.go** - 主入口编排：
```go
// Bootstrap 初始化所有组件并返回 Handlers
func Bootstrap(deps *Dependencies) (*Handlers, error) {
    // 1. 初始化 GORM Callback（必须在 Store 之前）
    if err := registerGORMCallbacks(deps.DB); err != nil {
        return nil, err
    }

    // 2. 初始化 Stores
    stores := initStores(deps)

    // 3. 初始化 Services
    services := initServices(stores)

    // 4. 初始化 Handlers
    handlers := initHandlers(services)

    return handlers, nil
}
```

**stores.go** - Store 层初始化：
```go
type Stores struct {
    Account         *postgres.AccountStore
    Role            *postgres.RoleStore
    Permission      *postgres.PermissionStore
    AccountRole     *postgres.AccountRoleStore
    RolePermission  *postgres.RolePermissionStore
    // TODO: 新增 Store 在此添加字段
}

func initStores(deps *Dependencies) *Stores {
    return &Stores{
        Account:        postgres.NewAccountStore(deps.DB, deps.Redis),
        Role:           postgres.NewRoleStore(deps.DB),
        Permission:     postgres.NewPermissionStore(deps.DB),
        AccountRole:    postgres.NewAccountRoleStore(deps.DB),
        RolePermission: postgres.NewRolePermissionStore(deps.DB),
        // TODO: 新增 Store 在此初始化
    }
}
```

**services.go** - Service 层初始化：
```go
type Services struct {
    Account    *accountSvc.Service
    Role       *roleSvc.Service
    Permission *permissionSvc.Service
    // TODO: 新增 Service 在此添加字段
}

func initServices(stores *Stores) *Services {
    return &Services{
        Account:    accountSvc.New(stores.Account, stores.Role, stores.AccountRole),
        Role:       roleSvc.New(stores.Role, stores.Permission, stores.RolePermission),
        Permission: permissionSvc.New(stores.Permission),
        // TODO: 新增 Service 在此初始化
    }
}
```

**handlers.go** - Handler 层初始化：
```go
func initHandlers(services *Services) *Handlers {
    return &Handlers{
        Account:    handler.NewAccountHandler(services.Account),
        Role:       handler.NewRoleHandler(services.Role),
        Permission: handler.NewPermissionHandler(services.Permission),
        // TODO: 新增 Handler 在此初始化
    }
}
```

**types.go** - 类型定义：
```go
// Handlers 封装所有 HTTP 处理器
type Handlers struct {
    Account    *handler.AccountHandler
    Role       *handler.RoleHandler
    Permission *handler.PermissionHandler
    // TODO: 新增 Handler 在此添加字段
}
```

**dependencies.go** - 基础依赖：
```go
// Dependencies 封装所有基础依赖
type Dependencies struct {
    DB     *gorm.DB
    Redis  *redis.Client
    Logger *zap.Logger
}
```

**main.go 简化后**：
```go
func main() {
    // 初始化基础依赖
    deps := initDependencies()

    // 一行完成所有业务组件初始化
    handlers, err := bootstrap.Bootstrap(deps)

    // 设置路由
    routes.Setup(app, handlers)

    // 启动服务
    app.Listen(":8080")
}
```

**理由**：
- **按层次拆分**：stores.go、services.go、handlers.go 职责清晰
- **易于扩展**：每层只需在对应文件中添加初始化代码
- **文件大小可控**：每个文件 < 100 行，避免单文件臃肿
- **main.go 零修改**：新增业务只修改 bootstrap 内部文件
- **符合 Go 风格**：显式依赖注入，不使用复杂的 DI 框架
- **TODO 标记清晰**：每层都有明确的扩展点标记

### Decision 3: 数据权限 GORM Callback 实现

**选择**：GORM Callback 自动化 + Context 绕过机制

**实现方案**：
```go
// pkg/gorm/callback.go

type contextKey string
const SkipDataPermissionKey contextKey = "skip_data_permission"

// SkipDataPermission 返回跳过数据权限过滤的 Context
func SkipDataPermission(ctx context.Context) context.Context {
    return context.WithValue(ctx, SkipDataPermissionKey, true)
}

// RegisterDataPermissionCallback 注册 GORM Callback
func RegisterDataPermissionCallback(db *gorm.DB, accountStore AccountStoreInterface) {
    db.Callback().Query().Before("gorm:query").Register("data_permission", func(tx *gorm.DB) {
        ctx := tx.Statement.Context

        // 检查是否跳过
        if skip, ok := ctx.Value(SkipDataPermissionKey).(bool); ok && skip {
            return
        }

        // 检查 root 用户
        if middleware.IsRootUser(ctx) {
            return
        }

        // 获取用户下级 ID 并应用过滤
        userID := middleware.GetUserIDFromContext(ctx)
        subordinateIDs, _ := accountStore.GetSubordinateIDs(ctx, userID)

        // 只对包含 owner_id 字段的表应用过滤
        if hasOwnerIDField(tx.Statement.Schema) {
            tx.Where("owner_id IN ?", subordinateIDs)
        }
    })
}
```

**使用方式**：
```go
// 正常查询 - 自动应用数据权限过滤
db.WithContext(ctx).Find(&accounts)

// 绕过权限过滤（如管理员操作、内部同步）
ctx = gorm.SkipDataPermission(ctx)
db.WithContext(ctx).Find(&accounts)
```

**理由**：
- 完全自动化，开发者无需手动调用 Scope
- 通过 Context 控制绕过，符合 Go 惯用模式
- 只对包含 owner_id 的表生效，安全可控
- 删除现有未使用的 scopes.go 代码

### Decision 4: 简化 AppError 结构

**选择**：删除 AppError.HTTPStatus 字段和 WithHTTPStatus() 方法

**问题分析**：
```go
type AppError struct {
    Code       int    // 业务错误码
    Message    string // 错误消息
    HTTPStatus int    // 冗余：总是从 Code 映射得到
    Err        error
}
```

**冗余之处**：
- HTTPStatus 字段总是通过 `GetHTTPStatus(code)` 从 Code 映射得到
- 存储 HTTPStatus 字段导致字段冗余
- WithHTTPStatus() 方法允许手动覆盖，可能导致状态码不一致

**优化方案**：
```go
type AppError struct {
    Code    int    // 业务错误码
    Message string // 错误消息
    Err     error  // 底层错误（可选）
}

// 删除 WithHTTPStatus() 方法
// ErrorHandler 中直接调用 GetHTTPStatus(e.Code) 获取状态码
```

**理由**：
- **减少字段冗余**：HTTPStatus 可以实时计算，不需要存储
- **消除不一致风险**：禁止手动设置状态码，确保 Code 和 HTTPStatus 始终匹配
- **简化 AppError**：只保留核心字段（Code, Message, Err）
- **保持职责分离**：AppError 只负责错误表示，HTTPStatus 由 ErrorHandler 处理

### Decision 5: 错误处理统一策略

**选择**：删除 response.Error()，统一使用全局 ErrorHandler

**当前格式分析**：
```go
// pkg/errors/handler.go - 已经统一使用 msg
c.Status(httpStatus).JSON(fiber.Map{
    "code":      code,
    "data":      nil,
    "msg":       message,  // 当前已是 msg
    "timestamp": time.Now().Format(time.RFC3339),
})

// pkg/response/response.go - 已经统一使用 msg
type Response struct {
    Code      int    `json:"code"`
    Data      any    `json:"data"`
    Message   string `json:"msg"`       // JSON 标签是 msg
    Timestamp string `json:"timestamp"`
}
```

**问题**：
- `response.Error()` 函数允许手动构造错误响应，导致两种错误处理方式混用
- 需要手动传递 `httpStatus` 参数，容易出错

**解决方案**：
```go
// pkg/response/response.go
// 删除 Error() 函数，只保留:
func Success(c *fiber.Ctx, data interface{}) error
func SuccessWithMessage(c *fiber.Ctx, data interface{}, message string) error
func SuccessWithPagination(c *fiber.Ctx, items any, total int64, page, size int) error
```

**Handler 统一写法**：
```go
func (h *AccountHandler) Create(c *fiber.Ctx) error {
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "请求参数格式错误")
    }

    if err := h.service.Create(ctx, &req); err != nil {
        return err  // 直接返回，由 ErrorHandler 处理
    }

    return response.Success(c, account)
}
```

**统一响应格式**（仅包含 4 个字段）：
```json
{
  "code": 0,
  "msg": "success",
  "data": {...},
  "timestamp": "2025-11-19T..."
}
```

**理由**：
- **消除两种错误处理方式**：Handler 只能返回 error，不能手动构造错误响应
- **格式已统一**：错误和成功响应都使用 `msg` 字段
- **简化开发**：错误码到 HTTP 状态码的映射由 ErrorHandler 统一处理
- **避免字段冗余**：不返回 `httpstatus` 字段（HTTP 状态码已在响应头中）

## Risks / Trade-offs

### Risk 1: GORM Callback 性能开销
**风险**：每次查询都执行 Callback 可能影响性能
**缓解**：
- GetSubordinateIDs 已实现 Redis 缓存（30分钟）
- 通过 Schema 检查只对需要的表生效
- 监控查询性能，必要时优化

### Risk 2: 删除代码可能影响未知依赖
**风险**：示例代码可能被测试或文档引用
**缓解**：
- 搜索确认无任何引用
- 删除后运行完整测试
- 项目处于框架搭建阶段，风险可控

### Risk 3: Bootstrap 多文件维护成本
**风险**：拆分成多个文件后，需要在多处添加新业务模块
**缓解**：
- TODO 注释明确标记所有扩展点
- 保持文件结构简单清晰（stores.go, services.go, handlers.go）
- 每个文件只负责一层初始化，职责单一
- 每个文件保持 < 100 行，易于理解和维护

## Migration Plan

1. **Phase 1（清理）**：
   - 删除示例代码（user/order）
   - 合并 Auth 实现
   - 验证现有功能不受影响

2. **Phase 2（解耦）**：
   - 创建 bootstrap 包
   - 重构 main.go
   - 验证启动流程正常

3. **Phase 3（自动化）**：
   - 实现 GORM Callback
   - 删除 scopes.go
   - 添加绕过机制测试

4. **Phase 4（规范化）**：
   - 统一错误格式
   - 删除 Error() 函数
   - 更新所有 Handler 写法

**回滚策略**：
- 使用 Git 分支，每个 Phase 可独立回滚
- 保留删除代码的备份（或通过 Git 历史恢复）

## Open Questions

1. **owner_id 字段检测**：如何优雅地检测表是否需要数据权限过滤？
   - 方案 A：检查 Schema 是否有 owner_id 字段
   - 方案 B：使用接口标记（如 `DataPermissionAware`）
   - 建议：先用方案 A，必要时再重构

2. **多租户支持**：shop_id 过滤是否也应该自动化？
   - 当前 DataPermissionScope 支持 shop_id
   - 建议：本次只自动化 owner_id，shop_id 作为 TODO

3. **Callback 注册时机**：应该在哪里注册 GORM Callback？
   - 建议：在 bootstrap 包初始化 DB 后立即注册
