From 888d3844f33f6e0de2d7cf8f42f13665c104414b Mon Sep 17 00:00:00 2001 From: wurenzhi Date: Fri, 20 Mar 2026 18:00:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(types):=20=E6=B7=BB=E5=8A=A0=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC=E5=92=8C=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 check-types.ps1 脚本用于检查项目各模块的类型定义 添加 migrate-types.ps1 脚本用于自动迁移类型导入路径 添加类型迁移指南文档说明迁移步骤和最佳实践 添加共享类型测试用例验证 schema 定义 --- docs/05_AI/08_Type_Migration_Guide.md | 376 ++++++++++++++++++ scripts/check-types.ps1 | 85 ++++ scripts/migrate-types.ps1 | 129 ++++++ .../shared/schemas/__tests__/schemas.test.ts | 247 ++++++++++++ 4 files changed, 837 insertions(+) create mode 100644 docs/05_AI/08_Type_Migration_Guide.md create mode 100644 scripts/check-types.ps1 create mode 100644 scripts/migrate-types.ps1 create mode 100644 server/src/shared/schemas/__tests__/schemas.test.ts diff --git a/docs/05_AI/08_Type_Migration_Guide.md b/docs/05_AI/08_Type_Migration_Guide.md new file mode 100644 index 0000000..5f1909e --- /dev/null +++ b/docs/05_AI/08_Type_Migration_Guide.md @@ -0,0 +1,376 @@ +# 类型导入迁移指南 + +> **目的**: 帮助开发者将旧的类型导入方式迁移到统一类型中心 +> **更新日期**: 2026-03-20 + +--- + +## 1. 迁移概览 + +### 1.1 迁移目标 + +``` +旧方式(分散) 新方式(统一) +───────────────────────────────────────────────────── +各模块独立定义类型 → 从类型中心导入 +手动维护类型一致性 → Schema 自动推导 +无运行时验证 → Zod 运行时验证 +``` + +### 1.2 迁移原则 + +1. **渐进式迁移**: 不要求一次性全部迁移 +2. **向后兼容**: 保留旧的类型导出路径 +3. **验证优先**: 迁移后必须通过类型检查 + +--- + +## 2. 后端迁移 + +### 2.1 旧导入方式 + +```typescript +// ❌ 旧方式:从本地文件导入 +import { User } from '../models/User'; +import { Product } from '../models/Product'; +import { Order } from '../models/Order'; + +// ❌ 旧方式:在文件中直接定义 +interface CreateUserRequest { + username: string; + email: string; + password: string; +} +``` + +### 2.2 新导入方式 + +```typescript +// ✅ 新方式:从统一类型中心导入 +import { User, Product, Order } from '@shared/types'; +import { CreateUserDTO, UpdateUserDTO } from '@shared/types'; +import { CreateUserSchema } from '@shared/schemas'; + +// ✅ 新方式:使用 Schema 验证 +const validated = CreateUserSchema.parse(requestBody); +``` + +### 2.3 迁移步骤 + +```bash +# 1. 查找需要迁移的文件 +grep -r "from '../models" server/src --include="*.ts" + +# 2. 替换导入语句 +# 将 '../models/User' 替换为 '@shared/types' + +# 3. 运行类型检查 +cd server && npx tsc --noEmit + +# 4. 修复类型错误 +``` + +--- + +## 3. 前端迁移 + +### 3.1 旧导入方式 + +```typescript +// ❌ 旧方式:从本地 types 导入 +import { User } from '../types/user'; +import { Product } from '../types/product'; + +// ❌ 旧方式:在组件中定义类型 +interface UserCardProps { + user: { + id: string; + name: string; + }; +} +``` + +### 3.2 新导入方式 + +```typescript +// ✅ 新方式:从 server 类型中心导入 +import { User, Product } from '@shared/types'; +import { UserDTO, ProductDTO } from '@shared/types'; + +// ✅ 新方式:使用类型化的 Props +import { User } from '@shared/types'; + +interface UserCardProps { + user: User; + onEdit?: (user: User) => void; +} +``` + +### 3.3 迁移步骤 + +```bash +# 1. 查找需要迁移的文件 +grep -r "from '../types" dashboard/src --include="*.ts" --include="*.tsx" + +# 2. 替换导入语句 +# 将 '../types/user' 替换为 '@shared/types' + +# 3. 运行类型检查 +cd dashboard && npx tsc --noEmit + +# 4. 修复类型错误 +``` + +--- + +## 4. 插件端迁移 + +### 4.1 旧导入方式 + +```typescript +// ❌ 旧方式:在插件中重复定义类型 +interface Message { + type: string; + payload: any; +} +``` + +### 4.2 新导入方式 + +```typescript +// ✅ 新方式:从 server 类型中心导入 +import { BaseMessage, MessageResponse } from '../../server/src/shared/types'; +import { BaseMessageSchema } from '../../server/src/shared/schemas'; + +// ✅ 新方式:使用 Schema 验证 +const validated = BaseMessageSchema.parse(message); +``` + +--- + +## 5. 常见迁移场景 + +### 5.1 类型扩展 + +```typescript +// ❌ 旧方式:扩展本地类型 +interface User { + id: string; + name: string; +} + +interface UserWithRole extends User { + role: string; +} + +// ✅ 新方式:从 Schema 扩展 +import { User } from '@shared/types'; +import { z } from 'zod'; + +const UserWithRoleSchema = UserSchema.extend({ + role: z.string() +}); + +type UserWithRole = z.infer; +``` + +### 5.2 类型组合 + +```typescript +// ❌ 旧方式:手动组合类型 +interface CreateUserRequest { + username: string; + email: string; +} + +interface UpdateUserRequest { + username?: string; + email?: string; +} + +// ✅ 新方式:使用 Schema 工具 +import { UserSchema } from '@shared/schemas'; +import { z } from 'zod'; + +const CreateUserSchema = UserSchema.pick({ + username: true, + email: true, + password: true +}); + +const UpdateUserSchema = UserSchema.partial().pick({ + username: true, + email: true +}); +``` + +### 5.3 API 响应类型 + +```typescript +// ❌ 旧方式:手动定义响应类型 +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +// ✅ 新方式:使用统一响应类型 +import { ApiResponse, PaginatedResult } from '@shared/types'; +import { SuccessResponseSchema, ErrorResponseSchema } from '@shared/schemas'; +``` + +--- + +## 6. 迁移检查清单 + +### 6.1 后端检查 + +- [ ] 所有 Service 层使用 `@shared/types` 导入 +- [ ] 所有 Controller 层使用 `@shared/schemas` 验证 +- [ ] 所有 Model 层类型从 Schema 推导 +- [ ] 运行 `npx tsc --noEmit` 无错误 + +### 6.2 前端检查 + +- [ ] 所有组件使用 `@shared/types` 导入 +- [ ] 所有 API 调用使用类型化的 DTO +- [ ] 所有 Props 使用类型中心定义 +- [ ] 运行 `npx tsc --noEmit` 无错误 + +### 6.3 插件端检查 + +- [ ] 所有消息类型从 server 导入 +- [ ] 所有消息使用 Schema 验证 +- [ ] 运行 `npx tsc --noEmit` 无错误 + +--- + +## 7. 迁移脚本 + +### 7.1 自动替换脚本 + +```bash +#!/bin/bash +# migrate-types.sh + +# 后端迁移 +find server/src -name "*.ts" -type f -exec sed -i \ + -e "s|from '../models/User'|from '@shared/types'|g" \ + -e "s|from '../models/Product'|from '@shared/types'|g" \ + -e "s|from '../models/Order'|from '@shared/types'|g" \ + {} + + +# 前端迁移 +find dashboard/src -name "*.ts" -o -name "*.tsx" -type f -exec sed -i \ + -e "s|from '../types/user'|from '@shared/types'|g" \ + -e "s|from '../types/product'|from '@shared/types'|g" \ + {} + + +echo "Migration complete. Please run: npx tsc --noEmit" +``` + +### 7.2 类型检查脚本 + +```bash +#!/bin/bash +# check-types.sh + +echo "Checking server types..." +cd server && npx tsc --noEmit --skipLibCheck +SERVER_EXIT=$? + +echo "Checking dashboard types..." +cd ../dashboard && npx tsc --noEmit --skipLibCheck +DASHBOARD_EXIT=$? + +echo "Checking extension types..." +cd ../extension && npx tsc --noEmit --skipLibCheck +EXTENSION_EXIT=$? + +if [ $SERVER_EXIT -ne 0 ] || [ $DASHBOARD_EXIT -ne 0 ] || [ $EXTENSION_EXIT -ne 0 ]; then + echo "❌ Type check failed" + exit 1 +else + echo "✅ All type checks passed" + exit 0 +fi +``` + +--- + +## 8. 常见问题 + +### Q1: 导入路径报错怎么办? + +**A**: 检查 `tsconfig.json` 中的 `paths` 配置是否正确: + +```json +{ + "compilerOptions": { + "paths": { + "@shared/types": ["../server/src/shared/types"], + "@shared/schemas": ["../server/src/shared/schemas"] + } + } +} +``` + +### Q2: 类型不匹配怎么办? + +**A**: 确保使用 Schema 推导类型,而不是手动定义: + +```typescript +// ❌ 手动定义可能与 Schema 不一致 +interface User { + id: string; + name: string; +} + +// ✅ 从 Schema 推导,保证一致 +import { User } from '@shared/types'; +// User 类型来自 UserSchema +``` + +### Q3: 运行时验证失败怎么办? + +**A**: 使用 Schema 的 `parse` 或 `safeParse` 方法: + +```typescript +import { CreateUserSchema } from '@shared/schemas'; + +// 安全解析 +const result = CreateUserSchema.safeParse(data); +if (!result.success) { + console.error(result.error.issues); + return; +} +const validated = result.data; +``` + +--- + +## 9. 版本兼容 + +### 9.1 向后兼容 + +旧的导入路径仍然可用,但建议迁移: + +```typescript +// 仍然可用(向后兼容) +import { User } from '../types/domain/User'; + +// 推荐使用(新方式) +import { User } from '@shared/types'; +``` + +### 9.2 废弃计划 + +| 版本 | 状态 | 说明 | +|------|------|------| +| 1.0.0 | 当前 | 新旧方式并存 | +| 1.1.0 | 计划 | 标记旧方式为 deprecated | +| 2.0.0 | 计划 | 移除旧的类型定义文件 | + +--- + +*迁移完成后,请更新 `server/src/shared/types/version.ts` 中的版本号* diff --git a/scripts/check-types.ps1 b/scripts/check-types.ps1 new file mode 100644 index 0000000..79757ff --- /dev/null +++ b/scripts/check-types.ps1 @@ -0,0 +1,85 @@ +#!/usr/bin/env pwsh +# check-types.ps1 - 类型检查脚本 +# 用法: ./scripts/check-types.ps1 + +$ErrorActionPreference = "Stop" +$ProjectRoot = Split-Path -Parent $PSScriptRoot + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " TypeScript Type Check Script" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +$ServerExit = 0 +$DashboardExit = 0 +$ExtensionExit = 0 +$NodeAgentExit = 0 + +# 检查 server +Write-Host "[1/4] Checking server types..." -ForegroundColor Yellow +Set-Location "$ProjectRoot/server" +try { + npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null + Write-Host " ✅ Server types OK" -ForegroundColor Green +} catch { + Write-Host " ❌ Server types FAILED" -ForegroundColor Red + npx tsc --noEmit --skipLibCheck 2>&1 + $ServerExit = 1 +} + +# 检查 dashboard +Write-Host "[2/4] Checking dashboard types..." -ForegroundColor Yellow +Set-Location "$ProjectRoot/dashboard" +try { + npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null + Write-Host " ✅ Dashboard types OK" -ForegroundColor Green +} catch { + Write-Host " ❌ Dashboard types FAILED" -ForegroundColor Red + npx tsc --noEmit --skipLibCheck 2>&1 + $DashboardExit = 1 +} + +# 检查 extension +Write-Host "[3/4] Checking extension types..." -ForegroundColor Yellow +Set-Location "$ProjectRoot/extension" +try { + npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null + Write-Host " ✅ Extension types OK" -ForegroundColor Green +} catch { + Write-Host " ❌ Extension types FAILED" -ForegroundColor Red + npx tsc --noEmit --skipLibCheck 2>&1 + $ExtensionExit = 1 +} + +# 检查 node-agent +Write-Host "[4/4] Checking node-agent types..." -ForegroundColor Yellow +if (Test-Path "$ProjectRoot/node-agent/tsconfig.json") { + Set-Location "$ProjectRoot/node-agent" + try { + npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null + Write-Host " ✅ Node-agent types OK" -ForegroundColor Green + } catch { + Write-Host " ❌ Node-agent types FAILED" -ForegroundColor Red + npx tsc --noEmit --skipLibCheck 2>&1 + $NodeAgentExit = 1 + } +} else { + Write-Host " ⏭️ Node-agent skipped (no tsconfig.json)" -ForegroundColor Gray +} + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan + +$TotalErrors = $ServerExit + $DashboardExit + $ExtensionExit + $NodeAgentExit + +if ($TotalErrors -eq 0) { + Write-Host " ✅ All type checks passed!" -ForegroundColor Green + Write-Host "========================================" -ForegroundColor Cyan + Set-Location $ProjectRoot + exit 0 +} else { + Write-Host " ❌ Type check failed with $TotalErrors error(s)" -ForegroundColor Red + Write-Host "========================================" -ForegroundColor Cyan + Set-Location $ProjectRoot + exit 1 +} diff --git a/scripts/migrate-types.ps1 b/scripts/migrate-types.ps1 new file mode 100644 index 0000000..9305f94 --- /dev/null +++ b/scripts/migrate-types.ps1 @@ -0,0 +1,129 @@ +#!/usr/bin/env pwsh +# migrate-types.ps1 - 类型导入迁移脚本 +# 用法: ./scripts/migrate-types.ps1 [-DryRun] + +param( + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" +$ProjectRoot = Split-Path -Parent $PSScriptRoot + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Type Import Migration Script" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +if ($DryRun) { + Write-Host "🔍 DRY RUN MODE - No files will be modified" -ForegroundColor Yellow + Write-Host "" +} + +# 定义迁移规则 +$MigrationRules = @( + @{ + Pattern = "from ['`"]\.\.\/models\/User['`"]" + Replacement = "from '@shared/types'" + Description = "User model import" + }, + @{ + Pattern = "from ['`"]\.\.\/models\/Product['`"]" + Replacement = "from '@shared/types'" + Description = "Product model import" + }, + @{ + Pattern = "from ['`"]\.\.\/models\/Order['`"]" + Replacement = "from '@shared/types'" + Description = "Order model import" + }, + @{ + Pattern = "from ['`"]\.\.\/types\/user['`"]" + Replacement = "from '@shared/types'" + Description = "User type import (frontend)" + }, + @{ + Pattern = "from ['`"]\.\.\/types\/product['`"]" + Replacement = "from '@shared/types'" + Description = "Product type import (frontend)" + }, + @{ + Pattern = "from ['`"]\.\.\/types\/order['`"]" + Replacement = "from '@shared/types'" + Description = "Order type import (frontend)" + } +) + +$TotalFiles = 0 +$TotalChanges = 0 + +# 迁移后端文件 +Write-Host "[1/2] Migrating server files..." -ForegroundColor Yellow +$ServerFiles = Get-ChildItem -Path "$ProjectRoot/server/src" -Include "*.ts" -Recurse + +foreach ($File in $ServerFiles) { + $Content = Get-Content $File.FullName -Raw + $OriginalContent = $Content + $FileChanged = $false + + foreach ($Rule in $MigrationRules) { + if ($Content -match $Rule.Pattern) { + $Content = $Content -replace $Rule.Pattern, $Rule.Replacement + Write-Host " 📝 $($File.Name): $($Rule.Description)" -ForegroundColor Gray + $FileChanged = $true + } + } + + if ($FileChanged) { + $TotalFiles++ + $TotalChanges++ + + if (-not $DryRun) { + Set-Content -Path $File.FullName -Value $Content -NoNewline + } + } +} + +# 迁移前端文件 +Write-Host "[2/2] Migrating dashboard files..." -ForegroundColor Yellow +$DashboardFiles = Get-ChildItem -Path "$ProjectRoot/dashboard/src" -Include "*.ts", "*.tsx" -Recurse + +foreach ($File in $DashboardFiles) { + $Content = Get-Content $File.FullName -Raw + $OriginalContent = $Content + $FileChanged = $false + + foreach ($Rule in $MigrationRules) { + if ($Content -match $Rule.Pattern) { + $Content = $Content -replace $Rule.Pattern, $Rule.Replacement + Write-Host " 📝 $($File.Name): $($Rule.Description)" -ForegroundColor Gray + $FileChanged = $true + } + } + + if ($FileChanged) { + $TotalFiles++ + $TotalChanges++ + + if (-not $DryRun) { + Set-Content -Path $File.FullName -Value $Content -NoNewline + } + } +} + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Migration Summary" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Files processed: $TotalFiles" -ForegroundColor White +Write-Host " Total changes: $TotalChanges" -ForegroundColor White + +if ($DryRun) { + Write-Host "" + Write-Host " This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow +} else { + Write-Host "" + Write-Host " ✅ Migration complete!" -ForegroundColor Green + Write-Host " Please run: ./scripts/check-types.ps1" -ForegroundColor Yellow +} + +Write-Host "========================================" -ForegroundColor Cyan diff --git a/server/src/shared/schemas/__tests__/schemas.test.ts b/server/src/shared/schemas/__tests__/schemas.test.ts new file mode 100644 index 0000000..15e38b4 --- /dev/null +++ b/server/src/shared/schemas/__tests__/schemas.test.ts @@ -0,0 +1,247 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import { + UserSchema, + UserRoleSchema, + UserStatusSchema, + CreateUserSchema, + UpdateUserSchema +} from '@shared/schemas/user.schema'; +import { + ProductSchema, + ProductStatusSchema +} from '@shared/schemas/product.schema'; +import { + OrderSchema, + OrderStatusSchema +} from '@shared/schemas/order.schema'; +import { + BaseMessageSchema, + MessageTypeSchema, + MessageResponseSchema +} from '@shared/schemas/message.schema'; + +describe('User Schema', () => { + it('should validate valid user data', () => { + const validUser = { + id: '123', + name: 'John Doe', + email: 'john@example.com', + role: 'admin', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = UserSchema.safeParse(validUser); + expect(result.success).toBe(true); + }); + + it('should reject invalid email', () => { + const invalidUser = { + id: '123', + name: 'John Doe', + email: 'invalid-email', + role: 'admin', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = UserSchema.safeParse(invalidUser); + expect(result.success).toBe(false); + }); + + it('should reject invalid role', () => { + const invalidUser = { + id: '123', + name: 'John Doe', + email: 'john@example.com', + role: 'invalid_role', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = UserSchema.safeParse(invalidUser); + expect(result.success).toBe(false); + }); +}); + +describe('Product Schema', () => { + it('should validate valid product data', () => { + const validProduct = { + id: '123', + name: 'Test Product', + sku: 'SKU-001', + price: 99.99, + stock: 100, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = ProductSchema.safeParse(validProduct); + expect(result.success).toBe(true); + }); + + it('should reject negative price', () => { + const invalidProduct = { + id: '123', + name: 'Test Product', + sku: 'SKU-001', + price: -10, + stock: 100, + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = ProductSchema.safeParse(invalidProduct); + expect(result.success).toBe(false); + }); +}); + +describe('Order Schema', () => { + it('should validate valid order data', () => { + const validOrder = { + id: '123', + orderNumber: 'ORD-001', + status: 'pending', + items: [ + { + productId: 'p1', + productName: 'Product 1', + quantity: 2, + unitPrice: 50, + totalPrice: 100 + } + ], + totalAmount: 100, + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = OrderSchema.safeParse(validOrder); + expect(result.success).toBe(true); + }); +}); + +describe('Message Schema', () => { + it('should validate valid message', () => { + const validMessage = { + type: 'COLLECT_ORDERS', + payload: { shopId: 'shop1' }, + traceId: 'trace-123' + }; + + const result = BaseMessageSchema.safeParse(validMessage); + expect(result.success).toBe(true); + }); + + it('should reject invalid message type', () => { + const invalidMessage = { + type: 'INVALID_TYPE', + payload: {} + }; + + const result = BaseMessageSchema.safeParse(invalidMessage); + expect(result.success).toBe(false); + }); + + it('should validate message response', () => { + const validResponse = { + success: true, + data: { orders: [] }, + traceId: 'trace-123' + }; + + const result = MessageResponseSchema.safeParse(validResponse); + expect(result.success).toBe(true); + }); + + it('should validate error response', () => { + const errorResponse = { + success: false, + error: { + code: 'ERROR_001', + message: 'Something went wrong' + }, + traceId: 'trace-123' + }; + + const result = MessageResponseSchema.safeParse(errorResponse); + expect(result.success).toBe(true); + }); +}); + +describe('Type Inference', () => { + it('should infer correct types from schema', () => { + type User = z.infer; + type UserRole = z.infer; + type UserStatus = z.infer; + + const user: User = { + id: '123', + name: 'Test', + email: 'test@example.com', + role: 'admin', + status: 'active', + createdAt: new Date(), + updatedAt: new Date() + }; + + expect(user.role).toBe('admin'); + expect(user.status).toBe('active'); + }); +}); + +describe('Schema Extensions', () => { + it('should extend schema correctly', () => { + const ExtendedUserSchema = UserSchema.extend({ + department: z.string() + }); + + const validExtendedUser = { + id: '123', + name: 'John', + email: 'john@example.com', + role: 'admin', + status: 'active', + department: 'Engineering', + createdAt: new Date(), + updatedAt: new Date() + }; + + const result = ExtendedUserSchema.safeParse(validExtendedUser); + expect(result.success).toBe(true); + }); + + it('should pick fields from schema', () => { + const UserPreviewSchema = UserSchema.pick({ + id: true, + name: true, + email: true + }); + + const validPreview = { + id: '123', + name: 'John', + email: 'john@example.com' + }; + + const result = UserPreviewSchema.safeParse(validPreview); + expect(result.success).toBe(true); + }); + + it('should make schema partial', () => { + const PartialUserSchema = UserSchema.partial(); + + const validPartial = { + name: 'John' + }; + + const result = PartialUserSchema.safeParse(validPartial); + expect(result.success).toBe(true); + }); +});