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

1111 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 数据模型(如需修改)
```go
// 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`
**新增接口**:
```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`
```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`
```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`
```typescript
// 新增接口
export interface ManualXXX {
id: number;
title: string;
// ...
}
// 新增请求类型
export interface CreateManualXXXRequest {
title: string;
// ...
}
```
#### 3.2.2 API 服务
**文件**:`frontend/src/services/manualService.ts`
```typescript
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`(如需要)
```typescript
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`
```typescript
<Route
path="/projects/:projectId/manuals/xxx"
element={<ManualXXXPage />}
/>
```
### 3.3 iOS 改动(SwiftUI + MVVM)
#### 3.3.1 数据模型
**文件**:`Models/ManualModel.swift`
```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`
```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`
```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`
```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`
```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`
```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`
```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` 创建子任务:
```typescript
// 示例 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 性能影响分析
```markdown
## 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 代码规范
```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 代码规范
```swift
// ✅ 好的实践
@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):
```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):
```typescript
// ManualListPage.test.tsx
describe('ManualListPage', () => {
it('should render manual list', async () => {
render(<ManualListPage />);
expect(await screen.findByText('项目手册')).toBeInTheDocument();
});
});
```
**E2E 测试**(Playwright):
```typescript
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):
```swift
// 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):
```swift
// 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):
```swift
#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. 最后搜索文件模式(命名规范)
**示例**:
```bash
# 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>)`
---