473 lines
10 KiB
Markdown
473 lines
10 KiB
Markdown
# LinguaForge 開發指南
|
|
|
|
## 快速開始
|
|
|
|
### 前置需求
|
|
- Node.js 18+ LTS
|
|
- PostgreSQL 14+
|
|
- Redis 7+
|
|
- React Native CLI
|
|
- Xcode (iOS 開發)
|
|
- Android Studio (Android 開發)
|
|
|
|
### 專案初始化步驟
|
|
|
|
#### 1. 克隆專案
|
|
```bash
|
|
git clone https://github.com/your-org/linguaforge.git
|
|
cd linguaforge
|
|
```
|
|
|
|
#### 2. 後端設置
|
|
```bash
|
|
cd backend
|
|
npm install
|
|
cp .env.example .env
|
|
# 編輯 .env 設定資料庫連線等
|
|
|
|
# 執行資料庫遷移
|
|
npm run migration:run
|
|
|
|
# 啟動開發伺服器
|
|
npm run dev
|
|
```
|
|
|
|
#### 3. 前端設置
|
|
```bash
|
|
cd mobile
|
|
npm install
|
|
cd ios && pod install && cd ..
|
|
|
|
# iOS
|
|
npm run ios
|
|
|
|
# Android
|
|
npm run android
|
|
```
|
|
|
|
## 開發流程
|
|
|
|
### Git 分支策略
|
|
```
|
|
main # 生產環境
|
|
├── develop # 開發整合
|
|
├── feature/card-generation # 功能開發
|
|
├── feature/speech-assessment # 功能開發
|
|
└── hotfix/critical-bug # 緊急修復
|
|
```
|
|
|
|
### Commit 規範
|
|
```
|
|
feat: 新增詞卡生成功能
|
|
fix: 修復複習排程計算錯誤
|
|
docs: 更新 API 文件
|
|
style: 調整程式碼格式
|
|
refactor: 重構認證模組
|
|
test: 新增單元測試
|
|
chore: 更新相依套件
|
|
```
|
|
|
|
## 核心功能實作指南
|
|
|
|
### 1. SM-2 間隔重複演算法
|
|
```typescript
|
|
interface SM2Result {
|
|
interval: number;
|
|
repetition: number;
|
|
easinessFactor: number;
|
|
}
|
|
|
|
function calculateSM2(
|
|
quality: number, // 0-5 的評分
|
|
repetition: number, // 已複習次數
|
|
easinessFactor: number, // 難易度因子
|
|
interval: number // 當前間隔天數
|
|
): SM2Result {
|
|
|
|
// quality < 3 表示答錯,重置
|
|
if (quality < 3) {
|
|
return {
|
|
interval: 1,
|
|
repetition: 0,
|
|
easinessFactor
|
|
};
|
|
}
|
|
|
|
// 計算新的難易度因子
|
|
const newEF = Math.max(1.3,
|
|
easinessFactor + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)
|
|
);
|
|
|
|
// 計算新的間隔
|
|
let newInterval: number;
|
|
if (repetition === 0) {
|
|
newInterval = 1;
|
|
} else if (repetition === 1) {
|
|
newInterval = 6;
|
|
} else {
|
|
newInterval = Math.round(interval * newEF);
|
|
}
|
|
|
|
return {
|
|
interval: newInterval,
|
|
repetition: repetition + 1,
|
|
easinessFactor: newEF
|
|
};
|
|
}
|
|
```
|
|
|
|
### 2. Gemini API 整合
|
|
```typescript
|
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
|
|
class CardGeneratorService {
|
|
private genAI: GoogleGenerativeAI;
|
|
|
|
constructor() {
|
|
this.genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
|
|
}
|
|
|
|
async generateCard(sentence: string, targetWord: string) {
|
|
const model = this.genAI.getGenerativeModel({ model: "gemini-pro" });
|
|
|
|
const prompt = `
|
|
Given the sentence: "${sentence}"
|
|
Target word: "${targetWord}"
|
|
|
|
Generate a vocabulary card with:
|
|
1. Definition in Traditional Chinese
|
|
2. Part of speech
|
|
3. IPA pronunciation
|
|
4. 3 example sentences
|
|
5. Common collocations
|
|
6. Difficulty level (beginner/intermediate/advanced)
|
|
|
|
Return as JSON format.
|
|
`;
|
|
|
|
const result = await model.generateContent(prompt);
|
|
const response = await result.response;
|
|
return JSON.parse(response.text());
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Microsoft Speech Service 整合
|
|
```typescript
|
|
import * as sdk from 'microsoft-cognitiveservices-speech-sdk';
|
|
|
|
class PronunciationService {
|
|
private speechConfig: sdk.SpeechConfig;
|
|
|
|
constructor() {
|
|
this.speechConfig = sdk.SpeechConfig.fromSubscription(
|
|
process.env.SPEECH_KEY,
|
|
process.env.SPEECH_REGION
|
|
);
|
|
}
|
|
|
|
async assessPronunciation(
|
|
audioBuffer: Buffer,
|
|
referenceText: string
|
|
): Promise<AssessmentResult> {
|
|
const audioConfig = sdk.AudioConfig.fromWavFileInput(audioBuffer);
|
|
|
|
const pronunciationConfig = new sdk.PronunciationAssessmentConfig(
|
|
referenceText,
|
|
sdk.PronunciationAssessmentGradingSystem.HundredMark,
|
|
sdk.PronunciationAssessmentGranularity.Phoneme,
|
|
true
|
|
);
|
|
|
|
const recognizer = new sdk.SpeechRecognizer(
|
|
this.speechConfig,
|
|
audioConfig
|
|
);
|
|
|
|
pronunciationConfig.applyTo(recognizer);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
recognizer.recognizeOnceAsync(
|
|
result => {
|
|
const pronunciationResult = sdk.PronunciationAssessmentResult
|
|
.fromResult(result);
|
|
|
|
resolve({
|
|
accuracyScore: pronunciationResult.accuracyScore,
|
|
fluencyScore: pronunciationResult.fluencyScore,
|
|
completenessScore: pronunciationResult.completenessScore,
|
|
pronunciationScore: pronunciationResult.pronunciationScore
|
|
});
|
|
},
|
|
error => reject(error)
|
|
);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. 離線資料同步
|
|
```typescript
|
|
class OfflineSyncService {
|
|
async syncData() {
|
|
// 1. 檢查網路連線
|
|
const isOnline = await NetInfo.fetch();
|
|
if (!isOnline.isConnected) return;
|
|
|
|
// 2. 取得本地待同步資料
|
|
const pendingChanges = await localDB.getPendingChanges();
|
|
|
|
// 3. 批量上傳變更
|
|
const syncPromises = pendingChanges.map(change => {
|
|
switch (change.type) {
|
|
case 'CREATE':
|
|
return api.createCard(change.data);
|
|
case 'UPDATE':
|
|
return api.updateCard(change.id, change.data);
|
|
case 'DELETE':
|
|
return api.deleteCard(change.id);
|
|
}
|
|
});
|
|
|
|
// 4. 處理同步結果
|
|
const results = await Promise.allSettled(syncPromises);
|
|
|
|
// 5. 標記成功同步的項目
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
localDB.markAsSynced(pendingChanges[index].id);
|
|
}
|
|
});
|
|
|
|
// 6. 下載伺服器端更新
|
|
const serverUpdates = await api.getUpdates(lastSyncTime);
|
|
await localDB.applyServerUpdates(serverUpdates);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 測試策略
|
|
|
|
### 單元測試範例
|
|
```typescript
|
|
describe('SM2 Algorithm', () => {
|
|
it('should reset interval when quality < 3', () => {
|
|
const result = calculateSM2(2, 5, 2.5, 10);
|
|
expect(result.interval).toBe(1);
|
|
expect(result.repetition).toBe(0);
|
|
});
|
|
|
|
it('should increase interval for good performance', () => {
|
|
const result = calculateSM2(4, 2, 2.5, 6);
|
|
expect(result.interval).toBeGreaterThan(6);
|
|
});
|
|
});
|
|
```
|
|
|
|
### E2E 測試範例
|
|
```typescript
|
|
describe('Card Generation Flow', () => {
|
|
it('should generate card from sentence', async () => {
|
|
await element(by.id('new-card-button')).tap();
|
|
await element(by.id('sentence-input')).typeText(
|
|
'I need to abandon this habit'
|
|
);
|
|
await element(by.text('abandon')).tap();
|
|
await element(by.id('generate-button')).tap();
|
|
|
|
await waitFor(element(by.id('card-preview')))
|
|
.toBeVisible()
|
|
.withTimeout(5000);
|
|
|
|
await expect(element(by.text('放棄'))).toBeVisible();
|
|
});
|
|
});
|
|
```
|
|
|
|
## 效能優化建議
|
|
|
|
### 1. React Native 優化
|
|
```javascript
|
|
// 使用 memo 優化重渲染
|
|
const CardItem = React.memo(({ card, onPress }) => {
|
|
return (
|
|
<TouchableOpacity onPress={() => onPress(card.id)}>
|
|
<Text>{card.word}</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
}, (prevProps, nextProps) => {
|
|
return prevProps.card.id === nextProps.card.id;
|
|
});
|
|
|
|
// 使用 FlatList 優化長列表
|
|
<FlatList
|
|
data={cards}
|
|
keyExtractor={item => item.id}
|
|
renderItem={({ item }) => <CardItem card={item} />}
|
|
windowSize={10}
|
|
initialNumToRender={10}
|
|
maxToRenderPerBatch={10}
|
|
removeClippedSubviews={true}
|
|
/>
|
|
```
|
|
|
|
### 2. API 優化
|
|
```typescript
|
|
// 批量請求
|
|
router.post('/cards/batch', async (req, res) => {
|
|
const operations = req.body.operations;
|
|
|
|
const results = await Promise.all(
|
|
operations.map(op => processOperation(op))
|
|
);
|
|
|
|
res.json({ results });
|
|
});
|
|
|
|
// 資料快取
|
|
@Injectable()
|
|
export class CardService {
|
|
constructor(
|
|
@InjectRedis() private redis: Redis,
|
|
@InjectRepository(Card) private cardRepo: Repository<Card>
|
|
) {}
|
|
|
|
async getCard(id: string) {
|
|
// 檢查快取
|
|
const cached = await this.redis.get(`card:${id}`);
|
|
if (cached) return JSON.parse(cached);
|
|
|
|
// 從資料庫取得
|
|
const card = await this.cardRepo.findOne(id);
|
|
|
|
// 寫入快取
|
|
await this.redis.set(
|
|
`card:${id}`,
|
|
JSON.stringify(card),
|
|
'EX',
|
|
3600
|
|
);
|
|
|
|
return card;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 監控與除錯
|
|
|
|
### 日誌配置
|
|
```typescript
|
|
import winston from 'winston';
|
|
|
|
const logger = winston.createLogger({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
format: winston.format.json(),
|
|
transports: [
|
|
new winston.transports.File({
|
|
filename: 'error.log',
|
|
level: 'error'
|
|
}),
|
|
new winston.transports.File({
|
|
filename: 'combined.log'
|
|
})
|
|
]
|
|
});
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
logger.add(new winston.transports.Console({
|
|
format: winston.format.simple()
|
|
}));
|
|
}
|
|
```
|
|
|
|
### Sentry 整合
|
|
```typescript
|
|
import * as Sentry from '@sentry/node';
|
|
|
|
Sentry.init({
|
|
dsn: process.env.SENTRY_DSN,
|
|
environment: process.env.NODE_ENV,
|
|
tracesSampleRate: 1.0
|
|
});
|
|
|
|
// 錯誤捕獲中間件
|
|
app.use((err, req, res, next) => {
|
|
Sentry.captureException(err);
|
|
res.status(500).json({
|
|
error: 'Internal server error'
|
|
});
|
|
});
|
|
```
|
|
|
|
## 部署檢查清單
|
|
|
|
### 部署前檢查
|
|
- [ ] 所有測試通過
|
|
- [ ] 程式碼審查完成
|
|
- [ ] 更新版本號
|
|
- [ ] 更新 CHANGELOG
|
|
- [ ] 環境變數配置正確
|
|
- [ ] 資料庫遷移準備就緒
|
|
- [ ] API 文件更新
|
|
- [ ] 監控告警設置
|
|
|
|
### 部署步驟
|
|
```bash
|
|
# 1. 建立 Docker 映像
|
|
docker build -t linguaforge-api:v1.0.0 .
|
|
|
|
# 2. 推送至 registry
|
|
docker push registry.example.com/linguaforge-api:v1.0.0
|
|
|
|
# 3. 更新 Kubernetes 部署
|
|
kubectl set image deployment/api api=registry.example.com/linguaforge-api:v1.0.0
|
|
|
|
# 4. 監控部署狀態
|
|
kubectl rollout status deployment/api
|
|
|
|
# 5. 執行煙霧測試
|
|
npm run test:smoke
|
|
```
|
|
|
|
## 常見問題排查
|
|
|
|
### 問題: iOS 建置失敗
|
|
```bash
|
|
# 清理快取
|
|
cd ios
|
|
rm -rf Pods Podfile.lock
|
|
pod install --repo-update
|
|
cd ..
|
|
npm run ios -- --reset-cache
|
|
```
|
|
|
|
### 問題: Android 建置失敗
|
|
```bash
|
|
# 清理專案
|
|
cd android
|
|
./gradlew clean
|
|
cd ..
|
|
npm run android -- --reset-cache
|
|
```
|
|
|
|
### 問題: 資料庫連線失敗
|
|
```typescript
|
|
// 檢查連線池配置
|
|
{
|
|
type: 'postgres',
|
|
host: process.env.DB_HOST,
|
|
port: parseInt(process.env.DB_PORT),
|
|
username: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
synchronize: false,
|
|
logging: true,
|
|
entities: ['dist/**/*.entity.js'],
|
|
migrations: ['dist/migrations/*.js'],
|
|
extra: {
|
|
max: 20, // 連線池大小
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 2000,
|
|
}
|
|
}
|
|
``` |