Files
ai-proj-helper/skills-req/req-dev-plugin/skills/references/details.md
T
johnQ 237631147a chore(skill): 清理全部 23 条历史校验警告
- 11 个超 20KB 的 SKILL.md 拆分,大章节移入 references/details.md
- 7 处非标准字段(arguments/user_invocable)移入 metadata
- 修 req-dev 4 条相对链接;validate 跳过行内代码避免 ![](...) 误报
- validate-skills.py: 23 warns -> 0 warns,0 errors
2026-10-31 22:21:07 +08:00

30 KiB
Raw Blame History

skills 详细内容

⚠️ 既有契约与读模型迁移门禁(必须)

惨痛教训:新写链路把真实数据写入新表,旧 API 仍读原表,前端虽然没改却从此看不到新数据。“后端已成功执行”不等于“用户能看到执行结果”。

开发计划涉及新表、新账本、新写路径、替换存储模型或接口版本时,必须先完成以下门禁。未通过时,不得把“新建平行表/新建平行 API”写成默认方案。

1. 先证明旧模型不能复用

  • 已检查现有表、字段、索引、状态机和扩展字段(如 metadata/JSON)。
  • 已说明为什么增加字段、子表或现有类型无法满足需求。
  • 新表承载的是真正独立的实体或生命周期,而不是为同一业务实体建立第二个彼此脱节的世界。
  • 已区分“内部不可变证据/事件账本”和“产品对外读模型”;新建内部账本不代表必须更换原有产品接口。

2. 真实调用方查证

必须从对外接口向所有消费方反向追踪,不能只审查新后端写路径:

  • 列出现有 API 路径、请求参数、响应结构和身份/分页语义。
  • 使用 rg 查找 Web、iOS、Android、定时任务、报表和第三方调用方。
  • 若仓库存在 .codex-fe-ref/,必须在其中查证配套前端的真实调用。
  • 开发计划的变更文件清单必须覆盖“写入 → 投影/查询 → API → 页面”完整链路。

3. 稳定契约默认不变

  • 既有用户界面已消费的 API,默认保留路径和响应契约;除非用户明确批准破坏性迁移,不得通过新建版本接口把兼容成本转嫁给前端。
  • 同一业务概念只保留一个产品级读模型。内部账本可以拆分,但必须投影回既有读模型,或由原 API 无感聚合。
  • 日志、告警、信号或推测数据不得在查询降级时冒充业务事实(例如把 warning 当成交易记录)。

4. 不可避免新写模型时的强制迁移计划

若确有独立生命周期、不可变审计或 1:N/N:N 证据需求,可以增加新表,但开发计划必须同时包含:

  1. 写入兼容:双写,或从新账本向既有读模型做事务性/幂等投影。
  2. 历史回填:有界、可恢复、可重跑,且不猜测无法证明的归属。
  3. 去重身份:定义跨旧/新数据源的稳定业务键,并用数据库唯一约束或等价强保证防重。
  4. 读取切换:定义何时以新证据为准、旧数据如何兼容,以及结果数量/金额/状态对账门槛。
  5. 回滚策略:回滚新写入时仍能读取已产生的真实业务事实,不删除或隐藏已成功数据。
  6. 可观测性:监控新写入成功但旧读模型缺失、投影延迟、去重冲突和回填差异。

5. 用户可见性是必须验收项

测试不得只证明“新写路径成功”。至少要有一个真实业务记录贯穿测试,同时证明:

  • 新写路径产生了正确事实。
  • 原有对外 API 可以立即或在约定 SLA 内返回该记录。
  • 每个真实消费页面/客户端都能正确解码并展示。
  • 时间、方向、状态、数量、金额与唯一标识在数据库、API 和 UI 之间一致。
  • 部分失败、重试、重复投递、迟到数据和回滚后仍不丢数、不重复、不冒充。

开发计划必须把上述内容落成明确的变更文件、任务、测试用例和发布门禁,不得只在“风险”章节留一句提醒。


3. 技术方案设计

3.1 后端改动(Go)

3.1.1 数据模型(如需修改)

// backend/models/manual.go
type Manual struct {
    ID          uint      `gorm:"primarykey"`
    ProjectID   uint      `gorm:"not null;index"`
    Title       string    `gorm:"size:500;not null"`
    // 新增字段(如果需要)
}

3.1.2 Repository 层(数据访问)

文件:backend/database/manual_repository.go

新增方法:

  • GetManualByXXX() - 按条件查询
  • UpdateManualXXX() - 更新特定字段
  • BatchXXX() - 批量操作

实现要点:

  • 使用 GORM 查询构建器
  • 处理软删除(deleted_at)
  • 正确处理事务

3.1.3 Service 层(业务逻辑)

文件:backend/services/manual_service.go

新增方法:

  • CreateManualWithValidation() - 创建 + 业务校验
  • PublishManual() - 发布逻辑

业务规则:

  • 权限检查(使用 permission 包)
  • 数据校验(必填字段、格式检查)
  • 状态流转(Draft → Published)
  • 事务处理(多表操作)

3.1.4 Handler 层(HTTP 处理)

文件:backend/handlers/manual_handler.go

新增接口:

// POST /api/projects/:id/manuals/xxx
func (h *ManualHandler) CreateXXX(c *gin.Context) {
    // 1. 解析请求参数
    // 2. 参数验证
    // 3. 调用 Service 层
    // 4. 返回响应
}

要点:

  • 使用 c.ShouldBindJSON() 解析请求
  • 使用 validator 进行参数验证
  • 统一错误处理(使用 c.JSON())
  • 记录日志(使用 logger)

3.1.5 Route 层(路由注册)

文件:backend/routes/manual_routes.go

func RegisterManualRoutes(r *gin.RouterGroup, h *handlers.ManualHandler) {
    manuals := r.Group("/manuals")
    manuals.Use(middleware.Auth()) // 认证中间件
    {
        manuals.POST("/xxx", h.CreateXXX)
        // 新增路由...
    }
}

3.1.6 数据库迁移(如需要)

文件:backend/migrations/YYYYMMDDHHMMSS_add_xxx_to_manuals.up.sql

-- 新增字段
ALTER TABLE manuals ADD COLUMN xxx VARCHAR(255);

-- 新增索引
CREATE INDEX idx_manuals_xxx ON manuals(xxx);

-- 新增表(如需要)
CREATE TABLE manual_xxx (
    id BIGSERIAL PRIMARY KEY,
    manual_id BIGINT NOT NULL REFERENCES manuals(id),
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

3.2 前端改动(React + TypeScript)

3.2.1 类型定义

文件:frontend/src/types/manual.ts

// 新增接口
export interface ManualXXX {
  id: number;
  title: string;
  // ...
}

// 新增请求类型
export interface CreateManualXXXRequest {
  title: string;
  // ...
}

3.2.2 API 服务

文件:frontend/src/services/manualService.ts

export const manualApi = {
  // 新增 API 方法
  createXXX: async (
    projectId: number,
    data: CreateManualXXXRequest
  ): Promise<ManualXXX> => {
    const response = await api.post(
      `/projects/${projectId}/manuals/xxx`,
      data
    );
    return response.data;
  },
};

3.2.3 组件开发

文件:frontend/src/components/ManualXXXCard.tsx(如需要)

interface ManualXXXCardProps {
  manual: Manual;
  onXXX?: (manual: Manual) => void;
}

const ManualXXXCard: React.FC<ManualXXXCardProps> = ({
  manual,
  onXXX
}) => {
  // 组件实现
};

3.2.4 页面修改

文件:frontend/src/pages/ManualListPage.tsx

改动点:

  • 新增状态:const [xxxState, setXxxState] = useState(...)
  • 新增处理函数:const handleXXX = async () => { ... }
  • 修改渲染逻辑:在合适位置添加新功能 UI

3.2.5 路由配置(如需要)

文件:frontend/src/App.tsx

<Route
  path="/projects/:projectId/manuals/xxx"
  element={<ManualXXXPage />}
/>

3.3 iOS 改动(SwiftUI + MVVM)

3.3.1 数据模型

文件:Models/ManualModel.swift

import Foundation

struct Manual: Codable, Identifiable {
    let id: Int
    let title: String
    let content: String?
    let status: ManualStatus
    let createdAt: Date
    let updatedAt: Date

    enum CodingKeys: String, CodingKey {
        case id, title, content, status
        case createdAt = "created_at"
        case updatedAt = "updated_at"
    }
}

enum ManualStatus: String, Codable {
    case draft
    case published
    case archived
}

3.3.2 DTO(API 响应)

文件:Models/DTOs/ManualListResponse.swift

import Foundation

struct ManualListResponse: Codable {
    let success: Bool
    let message: String
    let data: ManualListData
}

struct ManualListData: Codable {
    let data: [Manual]
    let pagination: Pagination
}

struct Pagination: Codable {
    let page: Int
    let pageSize: Int
    let total: Int
    let totalPages: Int

    enum CodingKeys: String, CodingKey {
        case page
        case pageSize = "page_size"
        case total
        case totalPages = "total_pages"
    }
}

3.3.3 API 端点

文件:Core/Services/APIEndpoints.swift

// 在 APIEndpoints 枚举中添加
enum APIEndpoints {
    // ... 现有端点

    // 手册相关
    case manualList(projectId: Int, page: Int, pageSize: Int)
    case manualDetail(id: Int)
    case createManual(projectId: Int)
    case updateManual(id: Int)
    case deleteManual(id: Int)

    var path: String {
        switch self {
        case .manualList(let projectId, _, _):
            return "/projects/\(projectId)/manuals"
        case .manualDetail(let id):
            return "/manuals/\(id)"
        case .createManual(let projectId):
            return "/projects/\(projectId)/manuals"
        case .updateManual(let id):
            return "/manuals/\(id)"
        case .deleteManual(let id):
            return "/manuals/\(id)"
        }
    }

    var method: String {
        switch self {
        case .manualList, .manualDetail:
            return "GET"
        case .createManual:
            return "POST"
        case .updateManual:
            return "PUT"
        case .deleteManual:
            return "DELETE"
        }
    }
}

3.3.4 服务协议

文件:Core/Services/ServiceProtocols.swift

// 添加手册服务协议
protocol ManualServiceProtocol {
    func fetchManuals(projectId: Int, page: Int, pageSize: Int) async throws -> [Manual]
    func fetchManualDetail(id: Int) async throws -> Manual
    func createManual(_ manual: CreateManualRequest) async throws -> Manual
    func updateManual(id: Int, _ update: UpdateManualRequest) async throws -> Manual
    func deleteManual(id: Int) async throws
}

3.3.5 ViewModel

文件:Features/Manuals/ManualListViewModel.swift

import Foundation
import Combine

@MainActor
class ManualListViewModel: ObservableObject {
    // MARK: - Published Properties
    @Published var manuals: [Manual] = []
    @Published var isLoading = false
    @Published var error: String?
    @Published var hasMorePages = true

    // MARK: - Private Properties
    private let manualService: ManualServiceProtocol
    private let projectId: Int
    private var currentPage = 1
    private let pageSize = 20

    // MARK: - Initialization
    init(projectId: Int, manualService: ManualServiceProtocol) {
        self.projectId = projectId
        self.manualService = manualService
    }

    // MARK: - Public Methods
    func loadManuals() async {
        guard !isLoading else { return }

        isLoading = true
        error = nil
        currentPage = 1

        do {
            let result = try await manualService.fetchManuals(
                projectId: projectId,
                page: currentPage,
                pageSize: pageSize
            )
            manuals = result
            hasMorePages = result.count >= pageSize
        } catch {
            self.error = error.localizedDescription
        }

        isLoading = false
    }

    func loadMoreIfNeeded(currentItem: Manual) async {
        guard hasMorePages,
              !isLoading,
              let lastItem = manuals.last,
              currentItem.id == lastItem.id else { return }

        currentPage += 1
        isLoading = true

        do {
            let newManuals = try await manualService.fetchManuals(
                projectId: projectId,
                page: currentPage,
                pageSize: pageSize
            )
            manuals.append(contentsOf: newManuals)
            hasMorePages = newManuals.count >= pageSize
        } catch {
            self.error = error.localizedDescription
            currentPage -= 1
        }

        isLoading = false
    }
}

3.3.6 View

文件:Features/Manuals/ManualListView.swift

import SwiftUI

struct ManualListView: View {
    @StateObject private var viewModel: ManualListViewModel

    init(projectId: Int, manualService: ManualServiceProtocol) {
        _viewModel = StateObject(wrappedValue: ManualListViewModel(
            projectId: projectId,
            manualService: manualService
        ))
    }

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading && viewModel.manuals.isEmpty {
                    ProgressView("加载中...")
                } else if let error = viewModel.error {
                    ErrorView(message: error) {
                        Task { await viewModel.loadManuals() }
                    }
                } else {
                    manualList
                }
            }
            .navigationTitle("项目手册")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: { /* 新建手册 */ }) {
                        Image(systemName: "plus")
                    }
                }
            }
        }
        .task {
            await viewModel.loadManuals()
        }
    }

    private var manualList: some View {
        List {
            ForEach(viewModel.manuals) { manual in
                ManualRowView(manual: manual)
                    .onAppear {
                        Task {
                            await viewModel.loadMoreIfNeeded(currentItem: manual)
                        }
                    }
            }

            if viewModel.isLoading {
                HStack {
                    Spacer()
                    ProgressView()
                    Spacer()
                }
            }
        }
        .refreshable {
            await viewModel.loadManuals()
        }
    }
}

3.3.7 导航注册

文件:Core/Architecture/AppCoordinator.swift

// 在 AppCoordinator 中添加手册导航
case manualList(projectId: Int)
case manualDetail(id: Int)

// 在 navigationDestination 中处理
.navigationDestination(for: AppRoute.self) { route in
    switch route {
    case .manualList(let projectId):
        ManualListView(
            projectId: projectId,
            manualService: DIContainer.shared.manualService
        )
    case .manualDetail(let id):
        ManualDetailView(
            manualId: id,
            manualService: DIContainer.shared.manualService
        )
    // ... 其他路由
    }
}

4. 开发任务拆分

4.1 任务层级结构

[DEV-PLAN] 需求标题
├── [后端] Model 层改动
├── [后端] Repository 层改动
├── [后端] Service 层改动
├── [后端] Handler 层改动
├── [后端] Route 层改动
├── [后端] Migration 脚本
├── [Web前端] Types 定义
├── [Web前端] API Service 封装
├── [Web前端] 组件开发
├── [Web前端] 页面修改
├── [iOS] Model 层改动
├── [iOS] DTO 定义
├── [iOS] APIEndpoints 添加
├── [iOS] ViewModel 实现
├── [iOS] View 实现
└── [测试] 单元测试 + 集成测试

4.2 子任务创建规则

按文件拆分(推荐):

  • 每个文件对应一个子任务
  • 任务标题格式:[层级] 修改 文件名
  • 示例:[Handler] 修改 manual_handler.go 添加发布接口

按功能模块拆分(大改动):

  • 将相关文件归为一个模块
  • 任务标题格式:[模块] 功能描述
  • 示例:[手册发布] 实现发布功能(Service + Handler + Route)

4.3 子任务创建示例

使用 mcp__ai-proj__create_subtask 创建子任务:

// 示例 1:后端子任务
mcp__ai-proj__create_subtask({
  parentId: 5326, // 开发任务 ID
  title: "[Handler] 修改 manual_handler.go 添加发布接口"
})

// 示例 2:前端子任务
mcp__ai-proj__create_subtask({
  parentId: 5326,
  title: "[Page] 修改 ManualListPage.tsx 添加发布按钮"
})

// 示例 3:数据库子任务
mcp__ai-proj__create_subtask({
  parentId: 5326,
  title: "[Migration] 创建手册版本表"
})

5. 实施顺序指南

5.1 标准开发顺序(新功能)

第一阶段:数据层(1-2天)
├── 1. Model 层:定义数据结构
├── 2. Migration:创建/修改表
└── 3. Repository 层:实现数据访问

第二阶段:业务层(2-3天)
├── 4. Service 层:实现业务逻辑
└── 5. 编写单元测试

第三阶段:接口层(1-2天)
├── 6. Handler 层:实现 HTTP 接口
├── 7. Route 层:注册路由
└── 8. API 文档(Swagger)

第四阶段:Web 前端层(3-4天)
├── 9. Types:定义 TypeScript 接口
├── 10. Services:封装 API 调用
├── 11. Components:开发可复用组件
└── 12. Pages:实现业务页面

第五阶段:iOS 客户端(2-3天)
├── 13. Model:定义数据模型
├── 14. DTO:定义 API 响应结构
├── 15. APIEndpoints:添加 API 端点
├── 16. ViewModel:实现业务逻辑
└── 17. View:实现 SwiftUI 视图

第六阶段:测试与优化(1-2天)
├── 18. 后端单元测试
├── 19. 前端组件测试
├── 20. iOS 单元测试
└── 21. E2E/UI 测试

5.2 快速修复顺序(Bug Fix)

1. 定位问题代码
2. 修改对应层级(通常是 Service 或 Handler)
3. 修改前端页面(如果有前端问题)
4. 添加/修改测试用例
5. 验证修复效果

6. 技术风险评估

6.1 影响分析矩阵

改动类型 影响范围 风险等级 应对措施
新增字段(非必填) 低 低 添加 Migration,向后兼容
修改字段类型 中 中 数据迁移脚本,充分测试
删除字段 高 高 确认无依赖,先标记废弃
新增 API 接口 低 低 接口设计评审
修改 API 接口 中 中 保留旧接口,添加版本号
删除 API 接口 高 高 确认无调用,发布公告
新增前端页面 低 低 路由冲突检查
修改核心组件 高 高 充分测试所有使用场景

6.2 风险评估清单

数据库风险:

  • 是否需要数据迁移?
  • 是否影响现有数据完整性?
  • 是否需要回滚方案?
  • 是否有索引优化需求?

API 风险:

  • 是否破坏现有 API 兼容性?
  • 是否需要版本管理?
  • 是否有性能影响?
  • 是否需要限流保护?

前端风险:

  • 是否影响现有页面功能?
  • 是否有性能瓶颈(大列表、复杂计算)?
  • 是否兼容主流浏览器?
  • 是否响应式友好?

业务风险:

  • 是否影响核心业务流程?
  • 是否需要通知用户?
  • 是否需要灰度发布?
  • 是否有回退方案?

6.3 性能影响分析

## 7. 代码规范检查

### 7.1 Go 代码规范

```go
// ✅ 好的实践
func (r *ManualRepository) GetManualByID(id uint) (*models.Manual, error) {
    var manual models.Manual
    if err := r.db.First(&manual, id).Error; err != nil {
        return nil, err
    }
    return &manual, nil
}

// ❌ 不好的实践
func (r *ManualRepository) getManual(id uint) *models.Manual {
    var manual models.Manual
    r.db.First(&manual, id)
    return &manual // 未处理错误
}

检查清单:

  • 导出函数首字母大写
  • 错误处理完整(不忽略 error)
  • 使用指针接收器(receiver)
  • 注释清晰(godoc 格式)
  • 命名符合 Go 规范(驼峰式)

7.2 TypeScript 代码规范

// ✅ 好的实践
interface Manual {
  id: number;
  title: string;
}

export const manualApi = {
  async getManual(id: number): Promise<Manual> {
    try {
      const response = await api.get(`/manuals/${id}`);
      return response.data;
    } catch (error) {
      console.error('Failed to get manual:', error);
      throw error;
    }
  }
};

// ❌ 不好的实践
export const getManual = (id) => {
  return api.get('/manuals/' + id).then(r => r.data);
}

检查清单:

  • 明确的类型定义(避免 any)
  • 使用 async/await(而非 .then())
  • 错误处理完整
  • 使用 ES6+ 语法(const, 箭头函数)
  • 导出清晰(export const/interface)

7.3 Swift/iOS 代码规范

// ✅ 好的实践
@MainActor
class ManualListViewModel: ObservableObject {
    // MARK: - Published Properties
    @Published private(set) var manuals: [Manual] = []
    @Published private(set) var isLoading = false
    @Published var error: String?

    // MARK: - Private Properties
    private let manualService: ManualServiceProtocol

    // MARK: - Initialization
    init(manualService: ManualServiceProtocol) {
        self.manualService = manualService
    }

    // MARK: - Public Methods
    func loadManuals() async {
        guard !isLoading else { return }
        isLoading = true
        defer { isLoading = false }

        do {
            manuals = try await manualService.fetchManuals()
        } catch {
            self.error = error.localizedDescription
        }
    }
}

// ❌ 不好的实践
class ManualVM: ObservableObject {
    @Published var manuals = [Manual]()
    @Published var loading = false

    func load() {
        loading = true
        // 未处理错误,未使用 @MainActor
        Task {
            manuals = try! await service.fetch()
            loading = false
        }
    }
}

检查清单:

  • ViewModel 使用 @MainActor 确保主线程更新
  • 使用 MARK 注释分隔代码区块
  • Published 属性使用 private(set) 防止外部修改
  • 使用协议依赖注入(方便测试)
  • 使用 async/await 而非 completion handler
  • 使用 guard 提前返回
  • 使用 defer 确保状态重置
  • 错误使用 do-catch 完整处理

命名规范:

类型 规范 示例
文件名 大驼峰 ManualListView.swift
类/结构体 大驼峰 ManualListViewModel
协议 大驼峰 + Protocol 后缀 ManualServiceProtocol
函数/变量 小驼峰 loadManuals(), isLoading
常量 小驼峰 let pageSize = 20
枚举 case 小驼峰 case draft, case published

MVVM 层级规范:

层级 职责 命名规则
Model 数据结构 XXXModel.swift
DTO API 响应 XXXResponse.swift, XXXListResponse.swift
Service 业务逻辑 XXXService.swift, XXXServiceProtocol
ViewModel 视图状态 XXXViewModel.swift
View UI 渲染 XXXView.swift, XXXListView.swift

8. 测试策略

8.1 后端测试

单元测试(Go):

// backend/handlers/manual_handler_test.go
func TestManualHandler_CreateManual(t *testing.T) {
    // Arrange
    handler := setupTestHandler()

    // Act
    req := httptest.NewRequest("POST", "/manuals", body)
    w := httptest.NewRecorder()
    handler.CreateManual(w, req)

    // Assert
    assert.Equal(t, 200, w.Code)
}

集成测试:

  • 测试完整的 HTTP 请求流程
  • 测试数据库交互
  • 测试事务回滚

8.2 前端测试

组件测试(Jest + React Testing Library):

// ManualListPage.test.tsx
describe('ManualListPage', () => {
  it('should render manual list', async () => {
    render(<ManualListPage />);
    expect(await screen.findByText('项目手册')).toBeInTheDocument();
  });
});

E2E 测试(Playwright):

test('create manual flow', async ({ page }) => {
  await page.goto('/projects/1/manuals');
  await page.click('text=新建手册');
  await page.fill('[name="title"]', 'Test Manual');
  await page.click('text=提交');
  await expect(page.locator('text=Test Manual')).toBeVisible();
});

8.3 iOS 测试

单元测试(XCTest):

// ManualListViewModelTests.swift
import XCTest
@testable import AI_Proj_iOS

@MainActor
final class ManualListViewModelTests: XCTestCase {
    var viewModel: ManualListViewModel!
    var mockService: MockManualService!

    override func setUp() {
        super.setUp()
        mockService = MockManualService()
        viewModel = ManualListViewModel(
            projectId: 1,
            manualService: mockService
        )
    }

    func testLoadManuals_Success() async {
        // Arrange
        mockService.mockManuals = [
            Manual(id: 1, title: "Test Manual", ...)
        ]

        // Act
        await viewModel.loadManuals()

        // Assert
        XCTAssertEqual(viewModel.manuals.count, 1)
        XCTAssertNil(viewModel.error)
        XCTAssertFalse(viewModel.isLoading)
    }

    func testLoadManuals_Error() async {
        // Arrange
        mockService.shouldFail = true

        // Act
        await viewModel.loadManuals()

        // Assert
        XCTAssertTrue(viewModel.manuals.isEmpty)
        XCTAssertNotNil(viewModel.error)
    }
}

// Mock Service
class MockManualService: ManualServiceProtocol {
    var mockManuals: [Manual] = []
    var shouldFail = false

    func fetchManuals(projectId: Int, page: Int, pageSize: Int) async throws -> [Manual] {
        if shouldFail {
            throw NetworkError.serverError(500, "Mock error")
        }
        return mockManuals
    }
}

UI 测试(XCUITest):

// ManualListUITests.swift
import XCTest

final class ManualListUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUp() {
        continueAfterFailure = false
        app.launch()
    }

    func testManualListDisplays() {
        // Navigate to manual list
        app.buttons["手册"].tap()

        // Verify list appears
        let manualList = app.collectionViews["manualList"]
        XCTAssertTrue(manualList.waitForExistence(timeout: 5))

        // Verify at least one item
        XCTAssertTrue(manualList.cells.count > 0)
    }

    func testPullToRefresh() {
        app.buttons["手册"].tap()

        let manualList = app.collectionViews["manualList"]
        manualList.swipeDown()

        // Verify refresh indicator appears
        XCTAssertTrue(app.activityIndicators.firstMatch.exists)
    }
}

Preview 测试(SwiftUI Previews):

#Preview("Manual List - Loading") {
    ManualListView(
        projectId: 1,
        manualService: PreviewMocks.manualService
    )
}

#Preview("Manual List - With Data") {
    ManualListView(
        projectId: 1,
        manualService: PreviewMocks.manualServiceWithData
    )
}

#Preview("Manual List - Error") {
    ManualListView(
        projectId: 1,
        manualService: PreviewMocks.manualServiceWithError
    )
}

10. 最佳实践

10.1 代码探索策略

优先级排序:

  1. 先搜索完全匹配(精确关键词)
  2. 再搜索模糊匹配(相关关键词)
  3. 最后搜索文件模式(命名规范)

示例:

# 1. 精确搜索
Grep(pattern="Manual", path="backend/models")

# 2. 模糊搜索
Grep(pattern="手册|manual", path="backend")

# 3. 文件模式
Glob(pattern="**/*manual*.go")

10.2 任务拆分原则

SMART 原则:

  • Specific(具体):明确要修改哪个文件
  • Measurable(可衡量):清晰的完成标准
  • Achievable(可实现):单个任务 2-4 小时完成
  • Relevant(相关):与需求直接相关
  • Time-bound(有时限):预估工时

拆分粒度:

  • ✅ 好的粒度:[Handler] 修改 manual_handler.go 添加发布接口(2h)
  • ❌ 太粗:[后端] 实现所有后端功能
  • ❌ 太细:[Handler] 添加 import 语句

10.3 文档编写原则

  1. 准确性:基于代码探索的真实信息
  2. 完整性:覆盖所有必要环节(Model → Route)
  3. 可操作性:开发者可直接按文档实施
  4. 可追溯性:记录决策原因和依据

10.4 常见陷阱

❌ 陷阱1:过度设计

  • 问题:为简单需求设计复杂架构
  • 解决:遵循 KISS 原则,简单实现

❌ 陷阱2:忽略现有代码

  • 问题:重复造轮子,不复用已有实现
  • 解决:充分探索代码库,识别可复用代码

❌ 陷阱3:缺少风险评估

  • 问题:未考虑兼容性和性能影响
  • 解决:使用风险评估清单,提前识别风险

❌ 陷阱4:任务拆分不合理

  • 问题:任务太粗或太细,难以执行和跟踪
  • 解决:按 2-4 小时粒度拆分,遵循 SMART 原则

11. 项目特定规范(ai-proj)

11.1 后端目录结构

backend/
├── models/          # GORM 模型(单一职责)
├── database/        # Repository(数据访问)
├── services/        # Service(业务逻辑)
├── handlers/        # HTTP Handler(请求处理)
├── routes/          # 路由注册
├── middleware/      # 中间件(Auth, CORS, Logger)
├── migrations/      # SQL 迁移脚本
├── utils/           # 工具函数
└── config/          # 配置管理

11.2 命名规范

后端(Go):

  • 文件:小写下划线(manual_handler.go)
  • 结构体:大驼峰(ManualHandler)
  • 接口:大驼峰(ManualRepository)
  • 方法:大驼峰(GetManualByID)
  • 变量:小驼峰(manualID)

前端(TypeScript):

  • 文件:大驼峰(ManualListPage.tsx)
  • 组件:大驼峰(ManualCard)
  • 接口:大驼峰(Manual)
  • 函数:小驼峰(getManual)
  • 变量:小驼峰(manualList)

iOS(Swift):

  • 文件:大驼峰(ManualListView.swift)
  • 类/结构体:大驼峰(ManualListViewModel)
  • 协议:大驼峰 + Protocol(ManualServiceProtocol)
  • 函数:小驼峰(loadManuals())
  • 变量:小驼峰(isLoading)
  • 枚举 case:小驼峰(case draft)

11.3 Git 提交规范

feat(manual): add manual publishing feature (REQ-YYYYMMDD-XXXX)
fix(manual): fix manual list page routing issue (REQ-YYYYMMDD-XXXX)
refactor(manual): optimize manual query performance (REQ-YYYYMMDD-XXXX)
docs(manual): update manual API documentation (REQ-YYYYMMDD-XXXX)
test(manual): add manual handler unit tests (REQ-YYYYMMDD-XXXX)

格式:<type>(<scope>): <subject> (<REQ-ID>)