10 KiB
10 KiB
LinguaForge 開發指南
快速開始
前置需求
- Node.js 18+ LTS
- PostgreSQL 14+
- Redis 7+
- React Native CLI
- Xcode (iOS 開發)
- Android Studio (Android 開發)
專案初始化步驟
1. 克隆專案
git clone https://github.com/your-org/linguaforge.git
cd linguaforge
2. 後端設置
cd backend
npm install
cp .env.example .env
# 編輯 .env 設定資料庫連線等
# 執行資料庫遷移
npm run migration:run
# 啟動開發伺服器
npm run dev
3. 前端設置
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 間隔重複演算法
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 整合
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 整合
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. 離線資料同步
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);
}
}
測試策略
單元測試範例
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 測試範例
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 優化
// 使用 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 優化
// 批量請求
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;
}
}
監控與除錯
日誌配置
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 整合
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 文件更新
- 監控告警設置
部署步驟
# 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 建置失敗
# 清理快取
cd ios
rm -rf Pods Podfile.lock
pod install --repo-update
cd ..
npm run ios -- --reset-cache
問題: Android 建置失敗
# 清理專案
cd android
./gradlew clean
cd ..
npm run android -- --reset-cache
問題: 資料庫連線失敗
// 檢查連線池配置
{
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,
}
}