993 lines
24 KiB
Markdown
993 lines
24 KiB
Markdown
# 程式碼規範與開發標準
|
||
|
||
## 概述
|
||
建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。
|
||
|
||
## 通用開發原則
|
||
|
||
### 代碼品質原則
|
||
- [ ] **可讀性優先**: 代碼應該容易閱讀和理解
|
||
- [ ] **一致性**: 遵循統一的命名和格式規範
|
||
- [ ] **簡潔性**: 避免過度複雜的解決方案
|
||
- [ ] **可測試性**: 代碼結構便於單元測試
|
||
- [ ] **可維護性**: 考慮未來修改和擴展的便利性
|
||
|
||
### SOLID原則遵循
|
||
- [ ] **單一職責**: 每個函數/類只負責一個明確的功能
|
||
- [ ] **開放封閉**: 對擴展開放,對修改封閉
|
||
- [ ] **里氏替換**: 子類應該能夠替換父類
|
||
- [ ] **介面隔離**: 不應該依賴不需要的介面
|
||
- [ ] **依賴倒置**: 依賴抽象而非具體實現
|
||
|
||
## C# (.NET Core) 規範
|
||
|
||
### 基本格式規則
|
||
|
||
#### EditorConfig 配置
|
||
```ini
|
||
# .editorconfig
|
||
root = true
|
||
|
||
[*]
|
||
charset = utf-8
|
||
end_of_line = crlf
|
||
insert_final_newline = true
|
||
indent_style = space
|
||
indent_size = 4
|
||
trim_trailing_whitespace = true
|
||
|
||
[*.{cs,csx,vb,vbx}]
|
||
indent_size = 4
|
||
insert_final_newline = true
|
||
|
||
[*.{json,js,ts,tsx,css,scss,yml,yaml}]
|
||
indent_size = 2
|
||
```
|
||
|
||
#### .NET 分析器規則
|
||
```xml
|
||
<!-- Directory.Build.props -->
|
||
<Project>
|
||
<PropertyGroup>
|
||
<TargetFramework>net8.0</TargetFramework>
|
||
<Nullable>enable</Nullable>
|
||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||
<WarningsAsErrors />
|
||
<WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
|
||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||
<PrivateAssets>all</PrivateAssets>
|
||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||
</PackageReference>
|
||
</ItemGroup>
|
||
</Project>
|
||
```
|
||
|
||
### 命名規範
|
||
|
||
#### C# 命名慣例
|
||
```csharp
|
||
// ✅ 類別和方法使用PascalCase
|
||
public class UserService
|
||
{
|
||
public async Task<UserProfile> GetUserProfileAsync(Guid userId)
|
||
{
|
||
// 方法實現
|
||
}
|
||
|
||
public decimal CalculateMonthlyInterestRate(decimal principal, decimal rate)
|
||
{
|
||
return principal * rate / 12;
|
||
}
|
||
}
|
||
|
||
// ✅ 變數和參數使用camelCase
|
||
private readonly IUserRepository _userRepository;
|
||
private const int MaxRetryAttempts = 3;
|
||
|
||
public async Task<bool> ValidateUserAsync(string email, string password)
|
||
{
|
||
var isValidEmail = IsValidEmailFormat(email);
|
||
var hashedPassword = HashPassword(password);
|
||
|
||
return isValidEmail && await _userRepository.ValidateCredentialsAsync(email, hashedPassword);
|
||
}
|
||
|
||
// ❌ 避免的命名
|
||
private string data; // 太泛化
|
||
private int u; // 太簡短
|
||
private async Task GetUserProfileDataAsync() {} // 冗餘的Data後綴
|
||
```
|
||
|
||
#### 常數和列舉
|
||
```typescript
|
||
// ✅ 常數使用SCREAMING_SNAKE_CASE
|
||
const API_ENDPOINTS = {
|
||
USER_PROFILE: '/api/v1/users/profile',
|
||
DIALOGUE_START: '/api/v1/dialogues/start',
|
||
} as const;
|
||
|
||
const MAX_DIALOGUE_DURATION_MINUTES = 30;
|
||
const DEFAULT_PAGINATION_LIMIT = 20;
|
||
|
||
// ✅ 列舉使用PascalCase
|
||
enum DialogueStatus {
|
||
InProgress = 'in_progress',
|
||
Completed = 'completed',
|
||
Abandoned = 'abandoned',
|
||
}
|
||
|
||
enum UserSubscriptionPlan {
|
||
Free = 'free',
|
||
Basic = 'basic',
|
||
Premium = 'premium',
|
||
Professional = 'professional',
|
||
}
|
||
```
|
||
|
||
#### 類型定義
|
||
```typescript
|
||
// ✅ 介面使用PascalCase,以I開頭(可選)
|
||
interface UserProfile {
|
||
userId: string;
|
||
username: string;
|
||
email: string;
|
||
createdAt: Date;
|
||
subscription: UserSubscriptionPlan;
|
||
}
|
||
|
||
interface ApiResponse<T> {
|
||
success: boolean;
|
||
data: T | null;
|
||
message?: string;
|
||
error?: ApiError;
|
||
}
|
||
|
||
// ✅ 類型別名使用PascalCase
|
||
type DialogueAnalysis = {
|
||
grammarScore: number;
|
||
semanticScore: number;
|
||
fluencyScore: number;
|
||
overallScore: number;
|
||
feedback: string[];
|
||
};
|
||
|
||
type CreateDialogueRequest = {
|
||
scenarioId: string;
|
||
difficultyOverride?: string;
|
||
targetVocabulary?: string[];
|
||
};
|
||
```
|
||
|
||
### 函數撰寫規範
|
||
|
||
#### 函數設計原則
|
||
```typescript
|
||
// ✅ 函數應該小巧、單一職責
|
||
const validateEmailFormat = (email: string): boolean => {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(email);
|
||
};
|
||
|
||
const calculateDialogueScore = (
|
||
grammarScore: number,
|
||
semanticScore: number,
|
||
fluencyScore: number
|
||
): number => {
|
||
const weights = { grammar: 0.3, semantic: 0.4, fluency: 0.3 };
|
||
|
||
return Math.round(
|
||
grammarScore * weights.grammar +
|
||
semanticScore * weights.semantic +
|
||
fluencyScore * weights.fluency
|
||
);
|
||
};
|
||
|
||
// ✅ 使用純函數優於副作用函數
|
||
const createUserSlug = (username: string): string => {
|
||
return username
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.trim();
|
||
};
|
||
|
||
// ✅ 錯誤處理明確
|
||
const fetchUserProfile = async (userId: string): Promise<UserProfile> => {
|
||
try {
|
||
const response = await api.get(`/users/${userId}`);
|
||
|
||
if (!response.data) {
|
||
throw new Error('User profile not found');
|
||
}
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
logger.error('Failed to fetch user profile', { userId, error });
|
||
throw error;
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 異步處理規範
|
||
```typescript
|
||
// ✅ 使用async/await而非Promise.then
|
||
const processDialogueAnalysis = async (
|
||
dialogueId: string
|
||
): Promise<DialogueAnalysis> => {
|
||
const dialogue = await getDialogue(dialogueId);
|
||
const analysis = await analyzeDialogueWithAI(dialogue.messages);
|
||
const savedAnalysis = await saveAnalysisResults(dialogueId, analysis);
|
||
|
||
return savedAnalysis;
|
||
};
|
||
|
||
// ✅ 適當的錯誤處理和重試機制
|
||
const retryOperation = async <T>(
|
||
operation: () => Promise<T>,
|
||
maxRetries: number = 3,
|
||
delayMs: number = 1000
|
||
): Promise<T> => {
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
try {
|
||
return await operation();
|
||
} catch (error) {
|
||
if (attempt === maxRetries) {
|
||
throw error;
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
|
||
}
|
||
}
|
||
|
||
throw new Error('All retry attempts failed');
|
||
};
|
||
```
|
||
|
||
### React/React Native 組件規範
|
||
|
||
#### 組件結構
|
||
```tsx
|
||
// ✅ 組件檔案結構標準
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { View, Text, StyleSheet } from 'react-native';
|
||
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { Button } from '@/components/ui';
|
||
import { DialogueService } from '@/services';
|
||
import { updateDialogueProgress } from '@/store/slices/dialogueSlice';
|
||
|
||
import type { Dialogue, DialogueMessage } from '@/types';
|
||
|
||
// ✅ Props介面定義
|
||
interface DialogueChatProps {
|
||
dialogueId: string;
|
||
onDialogueComplete: (dialogue: Dialogue) => void;
|
||
isVisible: boolean;
|
||
}
|
||
|
||
// ✅ 組件主體
|
||
export const DialogueChat: React.FC<DialogueChatProps> = ({
|
||
dialogueId,
|
||
onDialogueComplete,
|
||
isVisible,
|
||
}) => {
|
||
// State declarations
|
||
const [inputText, setInputText] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
// Redux selectors
|
||
const dialogue = useAppSelector(state =>
|
||
state.dialogue.currentDialogue
|
||
);
|
||
const dispatch = useAppDispatch();
|
||
|
||
// Effects
|
||
useEffect(() => {
|
||
if (isVisible && dialogueId) {
|
||
loadDialogue();
|
||
}
|
||
}, [isVisible, dialogueId]);
|
||
|
||
// Handlers
|
||
const handleSendMessage = useCallback(async () => {
|
||
if (!inputText.trim()) return;
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const response = await DialogueService.sendMessage(dialogueId, inputText);
|
||
dispatch(updateDialogueProgress(response));
|
||
setInputText('');
|
||
} catch (error) {
|
||
// Error handling
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [dialogueId, inputText, dispatch]);
|
||
|
||
const loadDialogue = useCallback(async () => {
|
||
// Load dialogue logic
|
||
}, [dialogueId]);
|
||
|
||
// Early returns
|
||
if (!dialogue) {
|
||
return <LoadingSpinner />;
|
||
}
|
||
|
||
// Main render
|
||
return (
|
||
<View style={styles.container}>
|
||
<Text style={styles.title}>{dialogue.scenarioTitle}</Text>
|
||
{/* Component content */}
|
||
<Button
|
||
title="發送訊息"
|
||
onPress={handleSendMessage}
|
||
disabled={isLoading}
|
||
/>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// ✅ 樣式定義
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
padding: 16,
|
||
},
|
||
title: {
|
||
fontSize: 18,
|
||
fontWeight: 'bold',
|
||
marginBottom: 16,
|
||
},
|
||
});
|
||
```
|
||
|
||
#### Hooks使用規範
|
||
```tsx
|
||
// ✅ 自定義Hook範例
|
||
export const useDialogueAnalysis = (dialogueId: string) => {
|
||
const [analysis, setAnalysis] = useState<DialogueAnalysis | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const analyzeDialogue = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const result = await DialogueService.getAnalysis(dialogueId);
|
||
setAnalysis(result);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Analysis failed');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [dialogueId]);
|
||
|
||
useEffect(() => {
|
||
if (dialogueId) {
|
||
analyzeDialogue();
|
||
}
|
||
}, [analyzeDialogue, dialogueId]);
|
||
|
||
return {
|
||
analysis,
|
||
loading,
|
||
error,
|
||
refetch: analyzeDialogue
|
||
};
|
||
};
|
||
```
|
||
|
||
### API和服務層規範
|
||
|
||
#### API客戶端結構
|
||
```typescript
|
||
// ✅ API服務類設計
|
||
export class DialogueService {
|
||
private static readonly BASE_URL = '/api/v1/dialogues';
|
||
|
||
static async startDialogue(request: CreateDialogueRequest): Promise<Dialogue> {
|
||
const response = await apiClient.post<ApiResponse<Dialogue>>(
|
||
`${this.BASE_URL}/start`,
|
||
request
|
||
);
|
||
|
||
if (!response.data.success) {
|
||
throw new ApiError(
|
||
response.data.error?.message || 'Failed to start dialogue'
|
||
);
|
||
}
|
||
|
||
return response.data.data!;
|
||
}
|
||
|
||
static async sendMessage(
|
||
dialogueId: string,
|
||
message: string
|
||
): Promise<DialogueMessage> {
|
||
const response = await apiClient.post<ApiResponse<DialogueMessage>>(
|
||
`${this.BASE_URL}/${dialogueId}/message`,
|
||
{ message, message_type: 'text' }
|
||
);
|
||
|
||
return this.handleApiResponse(response);
|
||
}
|
||
|
||
private static handleApiResponse<T>(
|
||
response: ApiResponse<T>
|
||
): T {
|
||
if (!response.success || !response.data) {
|
||
throw new ApiError(
|
||
response.error?.message || 'API request failed'
|
||
);
|
||
}
|
||
|
||
return response.data;
|
||
}
|
||
}
|
||
|
||
// ✅ 錯誤處理類
|
||
export class ApiError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public statusCode?: number,
|
||
public code?: string
|
||
) {
|
||
super(message);
|
||
this.name = 'ApiError';
|
||
}
|
||
}
|
||
```
|
||
|
||
## 資料庫操作規範
|
||
|
||
### SQL查詢撰寫標準
|
||
|
||
#### 查詢可讀性
|
||
```sql
|
||
-- ✅ 好的SQL格式 - 關鍵字大寫、適當縮排
|
||
SELECT
|
||
u.user_id,
|
||
u.username,
|
||
u.total_score,
|
||
COUNT(d.dialogue_id) AS total_dialogues,
|
||
AVG(d.overall_score)::INTEGER AS avg_score
|
||
FROM users u
|
||
LEFT JOIN dialogues d ON u.user_id = d.user_id
|
||
AND d.status = 'completed'
|
||
WHERE u.status = 'active'
|
||
AND u.created_at >= CURRENT_DATE - INTERVAL '30 days'
|
||
GROUP BY u.user_id, u.username, u.total_score
|
||
HAVING COUNT(d.dialogue_id) >= 5
|
||
ORDER BY u.total_score DESC
|
||
LIMIT 100;
|
||
|
||
-- ❌ 避免的格式
|
||
select u.user_id,u.username,count(d.dialogue_id) from users u left join dialogues d on u.user_id=d.user_id where u.status='active' group by u.user_id,u.username;
|
||
```
|
||
|
||
#### 效能考量
|
||
```sql
|
||
-- ✅ 使用適當索引和條件
|
||
-- 確保WHERE條件中的欄位有索引
|
||
SELECT dialogue_id, created_at, overall_score
|
||
FROM dialogues
|
||
WHERE user_id = $1 -- 有索引
|
||
AND created_at >= $2 -- 有索引
|
||
AND status = 'completed' -- 有索引
|
||
ORDER BY created_at DESC
|
||
LIMIT 20;
|
||
|
||
-- ✅ 避免N+1查詢問題
|
||
WITH user_stats AS (
|
||
SELECT
|
||
user_id,
|
||
COUNT(*) as dialogue_count,
|
||
AVG(overall_score) as avg_score
|
||
FROM dialogues
|
||
WHERE status = 'completed'
|
||
GROUP BY user_id
|
||
)
|
||
SELECT
|
||
u.username,
|
||
COALESCE(us.dialogue_count, 0) as total_dialogues,
|
||
COALESCE(us.avg_score, 0) as average_score
|
||
FROM users u
|
||
LEFT JOIN user_stats us ON u.user_id = us.user_id
|
||
WHERE u.status = 'active';
|
||
```
|
||
|
||
### ORM使用規範 (以Prisma為例)
|
||
|
||
```typescript
|
||
// ✅ Prisma查詢最佳實踐
|
||
export class UserRepository {
|
||
|
||
// ✅ 使用事務處理相關操作
|
||
static async updateUserScoreAndLevel(
|
||
userId: string,
|
||
scoreIncrease: number
|
||
): Promise<User> {
|
||
return await prisma.$transaction(async (tx) => {
|
||
const user = await tx.user.findUniqueOrThrow({
|
||
where: { userId },
|
||
});
|
||
|
||
const newTotalScore = user.totalScore + scoreIncrease;
|
||
const newLevel = this.calculateUserLevel(newTotalScore);
|
||
|
||
return await tx.user.update({
|
||
where: { userId },
|
||
data: {
|
||
totalScore: newTotalScore,
|
||
currentLevel: newLevel,
|
||
updatedAt: new Date(),
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
// ✅ 使用include避免N+1問題
|
||
static async getUserWithRecentDialogues(
|
||
userId: string
|
||
): Promise<UserWithDialogues> {
|
||
return await prisma.user.findUniqueOrThrow({
|
||
where: { userId },
|
||
include: {
|
||
dialogues: {
|
||
where: {
|
||
status: 'completed',
|
||
createdAt: {
|
||
gte: subDays(new Date(), 7)
|
||
}
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 10,
|
||
},
|
||
subscription: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
// ✅ 適當的錯誤處理
|
||
private static calculateUserLevel(totalScore: number): string {
|
||
if (totalScore < 1000) return 'A1';
|
||
if (totalScore < 3000) return 'A2';
|
||
if (totalScore < 6000) return 'B1';
|
||
if (totalScore < 10000) return 'B2';
|
||
if (totalScore < 15000) return 'C1';
|
||
return 'C2';
|
||
}
|
||
}
|
||
```
|
||
|
||
## 測試規範
|
||
|
||
### 單元測試
|
||
```typescript
|
||
// ✅ 測試結構 - AAA模式 (Arrange, Act, Assert)
|
||
import { calculateDialogueScore } from '@/utils/scoring';
|
||
|
||
describe('calculateDialogueScore', () => {
|
||
it('should calculate correct weighted average score', () => {
|
||
// Arrange
|
||
const grammarScore = 90;
|
||
const semanticScore = 80;
|
||
const fluencyScore = 85;
|
||
const expectedScore = 84; // 90*0.3 + 80*0.4 + 85*0.3 = 84
|
||
|
||
// Act
|
||
const result = calculateDialogueScore(grammarScore, semanticScore, fluencyScore);
|
||
|
||
// Assert
|
||
expect(result).toBe(expectedScore);
|
||
});
|
||
|
||
it('should handle edge cases with zero scores', () => {
|
||
// Arrange & Act
|
||
const result = calculateDialogueScore(0, 0, 0);
|
||
|
||
// Assert
|
||
expect(result).toBe(0);
|
||
});
|
||
|
||
it('should round to nearest integer', () => {
|
||
// Arrange
|
||
const result = calculateDialogueScore(85, 87, 83); // Expected: 85.4 → 85
|
||
|
||
// Assert
|
||
expect(result).toBe(85);
|
||
});
|
||
});
|
||
```
|
||
|
||
### 整合測試
|
||
```typescript
|
||
// ✅ API整合測試
|
||
import request from 'supertest';
|
||
import { app } from '@/app';
|
||
import { setupTestDatabase, cleanupTestDatabase } from '@/test/setup';
|
||
|
||
describe('POST /api/v1/dialogues/start', () => {
|
||
beforeEach(async () => {
|
||
await setupTestDatabase();
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTestDatabase();
|
||
});
|
||
|
||
it('should start new dialogue successfully', async () => {
|
||
// Arrange
|
||
const requestBody = {
|
||
scenarioId: 'SC_Restaurant_01',
|
||
difficultyOverride: 'A2',
|
||
};
|
||
|
||
// Act
|
||
const response = await request(app)
|
||
.post('/api/v1/dialogues/start')
|
||
.set('Authorization', 'Bearer valid-jwt-token')
|
||
.send(requestBody)
|
||
.expect(201);
|
||
|
||
// Assert
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.data).toHaveProperty('dialogueId');
|
||
expect(response.body.data).toHaveProperty('sessionToken');
|
||
expect(response.body.data.scenarioId).toBe(requestBody.scenarioId);
|
||
});
|
||
|
||
it('should return 400 for invalid scenario ID', async () => {
|
||
// Act
|
||
const response = await request(app)
|
||
.post('/api/v1/dialogues/start')
|
||
.set('Authorization', 'Bearer valid-jwt-token')
|
||
.send({ scenarioId: 'INVALID_ID' })
|
||
.expect(400);
|
||
|
||
// Assert
|
||
expect(response.body.success).toBe(false);
|
||
expect(response.body.error.code).toBe('INVALID_SCENARIO');
|
||
});
|
||
});
|
||
```
|
||
|
||
## 版本控制規範
|
||
|
||
### Git工作流程
|
||
|
||
#### 分支策略 (Git Flow)
|
||
```bash
|
||
# ✅ 分支命名規範
|
||
main # 生產環境代碼
|
||
develop # 開發整合分支
|
||
feature/JIRA-123-user-auth # 功能開發
|
||
hotfix/fix-login-bug # 緊急修復
|
||
release/v1.2.0 # 發布準備
|
||
|
||
# ✅ 功能開發工作流程
|
||
git checkout develop
|
||
git pull origin develop
|
||
git checkout -b feature/JIRA-123-dialogue-analysis
|
||
# 進行開發...
|
||
git add .
|
||
git commit -m "feat: implement dialogue analysis scoring algorithm"
|
||
git push origin feature/JIRA-123-dialogue-analysis
|
||
# 創建Pull Request到develop分支
|
||
```
|
||
|
||
#### Commit Message規範
|
||
```bash
|
||
# ✅ 使用Conventional Commits格式
|
||
# 類型(範圍): 簡短描述
|
||
|
||
feat(api): add dialogue analysis endpoint
|
||
fix(ui): resolve button click not working on iOS
|
||
docs(readme): update installation instructions
|
||
style(components): fix linting issues in DialogueChat
|
||
refactor(auth): simplify JWT token validation
|
||
test(dialogue): add unit tests for scoring algorithm
|
||
chore(deps): update React Native to 0.72.4
|
||
|
||
# ✅ 完整範例
|
||
feat(dialogue): implement AI-powered grammar scoring
|
||
|
||
- Add OpenAI integration for grammar analysis
|
||
- Implement scoring algorithm with configurable weights
|
||
- Add error handling and retry logic
|
||
- Update dialogue model to store grammar scores
|
||
|
||
Fixes #123
|
||
```
|
||
|
||
### Code Review規範
|
||
|
||
#### PR檢查清單
|
||
```markdown
|
||
## Pull Request Checklist
|
||
|
||
### 功能性檢查
|
||
- [ ] 功能按需求正確實現
|
||
- [ ] 邊界條件和錯誤處理完善
|
||
- [ ] 相關測試已添加並通過
|
||
- [ ] 不會破壞現有功能
|
||
|
||
### 代碼品質
|
||
- [ ] 代碼遵循團隊規範
|
||
- [ ] 變數和函數命名清晰
|
||
- [ ] 沒有重複代碼
|
||
- [ ] 效能考量適當
|
||
|
||
### 文檔和註釋
|
||
- [ ] 複雜邏輯有適當註釋
|
||
- [ ] API文檔已更新
|
||
- [ ] README或相關文檔已更新
|
||
|
||
### 安全性
|
||
- [ ] 沒有敏感資訊洩漏
|
||
- [ ] 輸入驗證和清理適當
|
||
- [ ] 權限檢查正確
|
||
|
||
### 其他
|
||
- [ ] 資料庫遷移腳本(如需要)
|
||
- [ ] 環境變數文檔更新
|
||
- [ ] 部署注意事項說明
|
||
```
|
||
|
||
#### Review回饋準則
|
||
```markdown
|
||
# ✅ 建設性回饋範例
|
||
|
||
## 主要問題 (Must Fix)
|
||
- 🚨 **安全問題**: SQL注入風險,請使用參數化查詢
|
||
- 🚨 **效能問題**: N+1查詢問題,建議使用JOIN或預加載
|
||
- 🚨 **邏輯錯誤**: 條件判斷有問題,會導致錯誤的計算結果
|
||
|
||
## 建議改進 (Should Consider)
|
||
- 💡 **代碼組織**: 建議將此邏輯提取到單獨函數提高可讀性
|
||
- 💡 **錯誤處理**: 考慮添加更具體的錯誤訊息
|
||
- 💡 **測試覆蓋**: 建議添加邊界條件測試
|
||
|
||
## 小問題 (Nice to Have)
|
||
- 🎨 **代碼風格**: 變數命名可以更具描述性
|
||
- 🎨 **註釋**: 複雜算法建議添加註釋說明
|
||
```
|
||
|
||
## 環境配置標準
|
||
|
||
### 開發環境設定
|
||
|
||
#### package.json腳本
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"dev": "concurrently \"npm run dev:api\" \"npm run dev:mobile\"",
|
||
"dev:api": "nodemon --exec ts-node src/server.ts",
|
||
"dev:mobile": "expo start",
|
||
"build": "tsc && npm run build:mobile",
|
||
"build:mobile": "expo build:ios && expo build:android",
|
||
"test": "jest",
|
||
"test:watch": "jest --watch",
|
||
"test:coverage": "jest --coverage",
|
||
"lint": "eslint src/**/*.{ts,tsx}",
|
||
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
||
"type-check": "tsc --noEmit"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 環境變數管理
|
||
```bash
|
||
# ✅ .env.example - 範本文件
|
||
NODE_ENV=development
|
||
PORT=3001
|
||
|
||
# Database
|
||
DATABASE_URL=postgresql://user:password@localhost:5432/dramaling_dev
|
||
REDIS_URL=redis://localhost:6379
|
||
|
||
# External APIs
|
||
OPENAI_API_KEY=your_openai_api_key_here
|
||
STRIPE_SECRET_KEY=sk_test_your_stripe_key_here
|
||
|
||
# JWT
|
||
JWT_SECRET=your_jwt_secret_here
|
||
JWT_EXPIRES_IN=7d
|
||
|
||
# AWS
|
||
AWS_ACCESS_KEY_ID=your_aws_access_key
|
||
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
|
||
AWS_REGION=ap-northeast-1
|
||
S3_BUCKET_NAME=dramaling-assets-dev
|
||
```
|
||
|
||
```typescript
|
||
// ✅ 環境變數驗證
|
||
import { z } from 'zod';
|
||
|
||
const envSchema = z.object({
|
||
NODE_ENV: z.enum(['development', 'staging', 'production']),
|
||
PORT: z.string().transform(Number),
|
||
DATABASE_URL: z.string().url(),
|
||
OPENAI_API_KEY: z.string().min(1),
|
||
JWT_SECRET: z.string().min(32),
|
||
});
|
||
|
||
export const env = envSchema.parse(process.env);
|
||
```
|
||
|
||
## 文檔撰寫規範
|
||
|
||
### API文檔標準
|
||
```typescript
|
||
/**
|
||
* 開始新的對話練習
|
||
*
|
||
* @route POST /api/v1/dialogues/start
|
||
* @param {CreateDialogueRequest} request - 對話創建請求
|
||
* @param {string} request.scenarioId - 場景ID (必填)
|
||
* @param {string} [request.difficultyOverride] - 難度覆寫 (可選)
|
||
* @param {string[]} [request.targetVocabulary] - 目標詞彙列表 (可選)
|
||
* @returns {Promise<Dialogue>} 創建的對話物件
|
||
*
|
||
* @throws {ApiError} SCENARIO_NOT_FOUND - 場景不存在
|
||
* @throws {ApiError} SUBSCRIPTION_REQUIRED - 需要訂閱權限
|
||
* @throws {ApiError} DAILY_LIMIT_EXCEEDED - 超過每日使用限制
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const dialogue = await DialogueService.startDialogue({
|
||
* scenarioId: 'SC_Restaurant_01',
|
||
* difficultyOverride: 'A2',
|
||
* targetVocabulary: ['reservation', 'menu', 'order']
|
||
* });
|
||
* ```
|
||
*/
|
||
export const startDialogue = async (
|
||
request: CreateDialogueRequest
|
||
): Promise<Dialogue> => {
|
||
// 實現邏輯
|
||
};
|
||
```
|
||
|
||
### README撰寫規範
|
||
```markdown
|
||
# Drama Ling - 語言學習對話練習應用
|
||
|
||
## 專案概述
|
||
Drama Ling 是一款結合AI分析的情境對話練習應用,幫助用戶在真實場景中提升語言溝通能力。
|
||
|
||
## 技術棧
|
||
- **前端**: React Native + TypeScript
|
||
- **後端**: Node.js + Express + TypeScript
|
||
- **資料庫**: PostgreSQL + Redis
|
||
- **AI服務**: OpenAI GPT-4
|
||
- **雲端**: AWS (ECS + RDS + S3)
|
||
|
||
## 快速開始
|
||
|
||
### 環境要求
|
||
- Node.js 18+
|
||
- PostgreSQL 15+
|
||
- Redis 7+
|
||
- React Native 0.72+
|
||
|
||
### 安裝步驟
|
||
1. 複製專案
|
||
```bash
|
||
git clone https://github.com/company/dramaling-app.git
|
||
cd dramaling-app
|
||
```
|
||
|
||
2. 安裝依賴
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
3. 設定環境變數
|
||
```bash
|
||
cp .env.example .env
|
||
# 編輯 .env 填入實際配置
|
||
```
|
||
|
||
4. 資料庫設置
|
||
```bash
|
||
npm run db:migrate
|
||
npm run db:seed
|
||
```
|
||
|
||
5. 啟動開發服務
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
## 專案結構
|
||
```
|
||
src/
|
||
├── components/ # 共用UI組件
|
||
├── screens/ # 頁面組件
|
||
├── services/ # API和業務邏輯服務
|
||
├── store/ # Redux狀態管理
|
||
├── utils/ # 工具函數
|
||
├── types/ # TypeScript類型定義
|
||
└── constants/ # 常數定義
|
||
```
|
||
|
||
## 開發流程
|
||
1. 從`develop`分支創建feature分支
|
||
2. 遵循代碼規範和測試要求
|
||
3. 創建Pull Request並等待Review
|
||
4. 合併後自動部署到staging環境
|
||
|
||
## 測試
|
||
```bash
|
||
npm run test # 執行所有測試
|
||
npm run test:watch # 監視模式
|
||
npm run test:coverage # 測試覆蓋率報告
|
||
```
|
||
|
||
## 部署
|
||
- **Staging**: 自動部署當develop分支更新時
|
||
- **Production**: 手動部署當release分支創建時
|
||
|
||
## 貢獻指南
|
||
請閱讀 [CONTRIBUTING.md](CONTRIBUTING.md) 了解詳細的開發和貢獻流程。
|
||
|
||
## 授權
|
||
本專案採用 MIT 授權 - 詳見 [LICENSE](LICENSE) 文件
|
||
```
|
||
|
||
---
|
||
|
||
## 程式碼審查檢查清單
|
||
|
||
### 自我檢查項目
|
||
- [ ] 代碼遵循團隊規範和風格指南
|
||
- [ ] 所有函數和類都有適當的類型標註
|
||
- [ ] 複雜邏輯有清楚的註釋說明
|
||
- [ ] 錯誤處理和邊界條件考慮周全
|
||
- [ ] 單元測試覆蓋新增功能
|
||
- [ ] 沒有console.log或調試代碼殘留
|
||
- [ ] 沒有TODO或FIXME未處理
|
||
- [ ] 效能考量適當(避免不必要的重新渲染等)
|
||
|
||
### 團隊審查重點
|
||
- [ ] 架構設計合理性
|
||
- [ ] API設計的一致性
|
||
- [ ] 資料流和狀態管理
|
||
- [ ] 安全性考量
|
||
- [ ] 可維護性和擴展性
|
||
- [ ] 與現有代碼的整合度
|
||
|
||
---
|
||
|
||
## 待完成任務
|
||
|
||
### 高優先級
|
||
1. [ ] 建立ESLint和Prettier配置文件
|
||
2. [ ] 設置Pre-commit hooks強制代碼格式檢查
|
||
3. [ ] 建立代碼審查模板和流程
|
||
4. [ ] 設置自動化測試的CI流程
|
||
|
||
### 中優先級
|
||
1. [ ] 建立API文檔自動生成工具
|
||
2. [ ] 設置代碼覆蓋率報告和監控
|
||
3. [ ] 建立效能測試標準和工具
|
||
4. [ ] 制定安全代碼審查檢查清單
|
||
|
||
### 低優先級
|
||
1. [ ] 研究和引入靜態代碼分析工具
|
||
2. [ ] 建立自動化代碼品質評分系統
|
||
3. [ ] 探索AI輔助代碼審查工具
|
||
4. [ ] 建立團隊技術分享和最佳實踐文檔
|
||
|
||
---
|
||
|
||
**最後更新**: 2024年9月5日
|
||
**負責人**: 技術負責人
|
||
**審查週期**: 每月檢討和更新 |