dramaling-vocab-learning/00_starter/old/development-guide.md

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,
  }
}