Skip to content

測試數據生成指南

概述

本指南詳細說明如何使用 front-stage-vue 專案中的測試數據生成系統,為電商平台創建各種真實的測試場景和數據。

核心工具

Faker.js 整合

專案使用 @faker-js/faker 作為主要的測試數據生成工具:

typescript
import { faker } from '@faker-js/faker'

// 設定中文本地化
faker.setLocale('zh_TW')

// 設定固定種子,確保可重現的測試數據
faker.seed(12345)

數據生成服務

位於 src/api/seedFaker.ts 的核心數據生成服務:

typescript
export class TestDataGenerator {
  static generateCustomers(count: number): Customer[]
  static generateProducts(count: number): Product[]
  static generateOrders(customerId: string, count: number): Order[]
  static generateConversations(customerId: string, count: number): Conversation[]
}

👥 客戶數據生成

基本客戶資料

typescript
function generateCustomer(): Customer {
  const gender = faker.person.sexType()
  const firstName = faker.person.firstName(gender)
  const lastName = faker.person.lastName(gender)
  
  return {
    id: faker.string.uuid(),
    email: faker.internet.email({ firstName, lastName }),
    name: `${firstName} ${lastName}`,
    phone: faker.phone.number('09########'),
    dateOfBirth: faker.date.birthdate({ min: 18, max: 65, mode: 'age' }),
    avatar: faker.image.avatar(),
    
    // 地址資訊
    address: {
      street: faker.location.streetAddress(),
      city: faker.helpers.arrayElement([
        '台北市', '新北市', '桃園市', '台中市', 
        '台南市', '高雄市', '新竹市', '嘉義市'
      ]),
      district: faker.location.secondaryAddress(),
      zipCode: faker.location.zipCode('###')
    },
    
    // 客戶等級
    tier: faker.helpers.weightedArrayElement([
      { weight: 60, value: 'bronze' },
      { weight: 25, value: 'silver' },
      { weight: 12, value: 'gold' },
      { weight: 3, value: 'platinum' }
    ]),
    
    // 時間戳
    createdAt: faker.date.recent({ days: 365 }),
    lastLoginAt: faker.date.recent({ days: 30 })
  }
}

客戶行為特徵

typescript
function generateCustomerBehavior(customerId: string) {
  return {
    customerId,
    
    // 購物偏好
    preferences: {
      categories: faker.helpers.arrayElements([
        '3C電子', '服飾配件', '家居用品', '美妝保養', 
        '運動健身', '書籍文具', '食品飲料'
      ], { min: 1, max: 3 }),
      
      priceRange: faker.helpers.arrayElement([
        { min: 0, max: 1000 },
        { min: 1000, max: 3000 },
        { min: 3000, max: 10000 },
        { min: 10000, max: 50000 }
      ])
    },
    
    // 活躍度指標
    activity: {
      loginFrequency: faker.number.int({ min: 1, max: 30 }), // 每月登入次數
      avgOrderValue: faker.number.int({ min: 500, max: 8000 }), // 平均訂單金額
      purchaseFrequency: faker.number.int({ min: 1, max: 10 }) // 每月購買次數
    }
  }
}

🛍️ 商品數據生成

商品基本資訊

typescript
function generateProduct(): Product {
  const category = faker.helpers.arrayElement([
    '3C電子', '服飾配件', '家居用品', '美妝保養', 
    '運動健身', '書籍文具', '食品飲料'
  ])
  
  return {
    id: faker.string.uuid(),
    name: generateProductName(category),
    description: faker.commerce.productDescription(),
    category,
    
    // 價格資訊
    price: {
      original: faker.number.int({ min: 100, max: 10000 }),
      discount: faker.number.float({ min: 0.1, max: 0.5, precision: 0.01 }),
      currency: 'TWD'
    },
    
    // 庫存資訊
    inventory: {
      quantity: faker.number.int({ min: 0, max: 999 }),
      lowStockThreshold: faker.number.int({ min: 5, max: 20 }),
      isInStock: true // 根據 quantity 動態計算
    },
    
    // 商品圖片
    images: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => 
      faker.image.urlPicsumPhotos({ width: 400, height: 300 })
    ),
    
    // 規格選項
    variants: generateProductVariants(),
    
    // SEO 資訊
    seo: {
      slug: faker.helpers.slugify(faker.commerce.productName()),
      tags: faker.helpers.arrayElements([
        '熱門', '新品', '限時優惠', '免運費', '精選商品'
      ], { min: 0, max: 3 })
    },
    
    // 評價資訊
    rating: {
      average: faker.number.float({ min: 3.0, max: 5.0, precision: 0.1 }),
      count: faker.number.int({ min: 0, max: 500 })
    },
    
    createdAt: faker.date.recent({ days: 180 }),
    updatedAt: faker.date.recent({ days: 30 })
  }
}

function generateProductName(category: string): string {
  const nameTemplates = {
    '3C電子': () => `${faker.commerce.productAdjective()} ${faker.helpers.arrayElement([
      '智慧手機', '筆記型電腦', '無線耳機', '智能手錶', '平板電腦'
    ])}`,
    '服飾配件': () => `${faker.commerce.productAdjective()} ${faker.helpers.arrayElement([
      'T恤', '牛仔褲', '洋裝', '運動鞋', '手提包'
    ])}`,
    '家居用品': () => `${faker.commerce.productAdjective()} ${faker.helpers.arrayElement([
      '收納盒', '抱枕', '檯燈', '花瓶', '地毯'
    ])}`
    // ... 其他分類
  }
  
  return nameTemplates[category]?.() || faker.commerce.productName()
}

商品變體

typescript
function generateProductVariants() {
  const variantTypes = ['顏色', '尺寸', '規格']
  const selectedTypes = faker.helpers.arrayElements(variantTypes, { min: 1, max: 2 })
  
  return selectedTypes.map(type => ({
    type,
    options: generateVariantOptions(type)
  }))
}

function generateVariantOptions(type: string) {
  const optionMap = {
    '顏色': ['黑色', '白色', '紅色', '藍色', '綠色'],
    '尺寸': ['S', 'M', 'L', 'XL', 'XXL'],
    '規格': ['標準版', '豪華版', '專業版']
  }
  
  return faker.helpers.arrayElements(optionMap[type], { min: 2, max: 4 })
}

📦 訂單數據生成

訂單基本資訊

typescript
function generateOrder(customerId: string): Order {
  const orderItems = generateOrderItems()
  const subtotal = orderItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  const shipping = subtotal > 1000 ? 0 : 100 // 滿千免運
  const total = subtotal + shipping
  
  return {
    id: faker.string.uuid(),
    orderNumber: generateOrderNumber(),
    customerId,
    
    // 訂單項目
    items: orderItems,
    
    // 金額資訊
    pricing: {
      subtotal,
      shipping,
      discount: 0,
      tax: Math.round(total * 0.05), // 5% 營業稅
      total: total + Math.round(total * 0.05)
    },
    
    // 訂單狀態
    status: faker.helpers.weightedArrayElement([
      { weight: 20, value: 'pending' },      // 待處理
      { weight: 30, value: 'processing' },   // 處理中
      { weight: 25, value: 'shipped' },      // 已出貨
      { weight: 20, value: 'delivered' },    // 已送達
      { weight: 3, value: 'cancelled' },     // 已取消
      { weight: 2, value: 'returned' }       // 已退貨
    ]),
    
    // 配送資訊
    shipping: {
      method: faker.helpers.arrayElement(['宅配', '超商取貨', '門市自取']),
      address: generateShippingAddress(),
      trackingNumber: faker.string.alphanumeric(10).toUpperCase()
    },
    
    // 付款資訊
    payment: {
      method: faker.helpers.arrayElement(['信用卡', 'ATM轉帳', '超商代碼', '貨到付款']),
      status: faker.helpers.arrayElement(['pending', 'paid', 'failed', 'refunded']),
      transactionId: faker.string.uuid()
    },
    
    // 時間戳記
    createdAt: faker.date.recent({ days: 90 }),
    updatedAt: faker.date.recent({ days: 7 }),
    
    // 備註
    notes: faker.datatype.boolean(0.3) ? faker.lorem.sentence() : null
  }
}

function generateOrderNumber(): string {
  const date = new Date()
  const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '')
  const randomStr = faker.string.numeric(6)
  return `ORD${dateStr}${randomStr}`
}

function generateOrderItems(): OrderItem[] {
  const itemCount = faker.number.int({ min: 1, max: 5 })
  
  return Array.from({ length: itemCount }, () => {
    const product = generateProduct() // 簡化版商品
    
    return {
      id: faker.string.uuid(),
      productId: product.id,
      productName: product.name,
      variant: faker.helpers.arrayElement(['標準', '黑色/M', '白色/L']),
      price: product.price.original,
      quantity: faker.number.int({ min: 1, max: 3 }),
      subtotal: product.price.original * faker.number.int({ min: 1, max: 3 })
    }
  })
}

💬 客服對話生成

對話數據

typescript
function generateConversation(customerId: string): Conversation {
  const subjects = [
    '商品問題諮詢', '退換貨申請', '付款問題', 
    '配送問題', '帳號問題', '優惠活動諮詢'
  ]
  
  return {
    id: faker.string.uuid(),
    customerId,
    agentId: faker.string.uuid(),
    subject: faker.helpers.arrayElement(subjects),
    
    // 對話狀態
    status: faker.helpers.weightedArrayElement([
      { weight: 40, value: 'open' },      // 進行中
      { weight: 45, value: 'resolved' },  // 已解決
      { weight: 10, value: 'pending' },   // 待處理
      { weight: 5, value: 'closed' }      // 已關閉
    ]),
    
    // 優先級
    priority: faker.helpers.weightedArrayElement([
      { weight: 60, value: 'low' },
      { weight: 30, value: 'medium' },
      { weight: 8, value: 'high' },
      { weight: 2, value: 'urgent' }
    ]),
    
    // 對話訊息
    messages: generateConversationMessages(),
    
    // 標籤
    tags: faker.helpers.arrayElements([
      '退款', '換貨', '技術問題', '建議', '投訴'
    ], { min: 0, max: 2 }),
    
    // 滿意度評分
    rating: faker.datatype.boolean(0.7) ? 
      faker.number.int({ min: 3, max: 5 }) : null,
    
    createdAt: faker.date.recent({ days: 30 }),
    updatedAt: faker.date.recent({ days: 7 })
  }
}

function generateConversationMessages(): Message[] {
  const messageCount = faker.number.int({ min: 2, max: 8 })
  const messages: Message[] = []
  
  for (let i = 0; i < messageCount; i++) {
    const isCustomerMessage = i % 2 === 0
    
    messages.push({
      id: faker.string.uuid(),
      sender: isCustomerMessage ? 'customer' : 'agent',
      content: isCustomerMessage ? 
        generateCustomerMessage() : 
        generateAgentMessage(),
      timestamp: faker.date.recent({ days: 7 }),
      attachments: faker.datatype.boolean(0.1) ? 
        [faker.image.urlPicsumPhotos({ width: 300, height: 200 })] : []
    })
  }
  
  return messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
}

數據生成腳本

批量生成腳本

typescript
// src/api/seedFaker.ts
export class BatchDataGenerator {
  /**
   * 生成完整的測試數據集
   */
  static async generateFullDataset() {
    console.log('🚀 開始生成測試數據集...')
    
    try {
      // 1. 生成客戶數據
      const customers = this.generateCustomers(50)
      await this.saveCustomers(customers)
      console.log(`✅ 已生成 ${customers.length} 位客戶`)
      
      // 2. 生成商品數據
      const products = this.generateProducts(200)
      await this.saveProducts(products)
      console.log(`✅ 已生成 ${products.length} 項商品`)
      
      // 3. 生成訂單數據
      let totalOrders = 0
      for (const customer of customers) {
        const orderCount = faker.number.int({ min: 0, max: 5 })
        const orders = this.generateOrders(customer.id, orderCount)
        await this.saveOrders(orders)
        totalOrders += orders.length
      }
      console.log(`✅ 已生成 ${totalOrders} 筆訂單`)
      
      // 4. 生成客服對話
      let totalConversations = 0
      for (const customer of customers) {
        if (faker.datatype.boolean(0.3)) { // 30% 客戶有客服記錄
          const convCount = faker.number.int({ min: 1, max: 3 })
          const conversations = this.generateConversations(customer.id, convCount)
          await this.saveConversations(conversations)
          totalConversations += conversations.length
        }
      }
      console.log(`✅ 已生成 ${totalConversations} 個客服對話`)
      
      console.log('🎉 測試數據生成完成!')
      
    } catch (error) {
      console.error('❌ 數據生成失敗:', error)
      throw error
    }
  }
  
  /**
   * 清理測試數據
   */
  static async cleanTestData() {
    console.log('🧹 開始清理測試數據...')
    
    const tables = ['conversations', 'orders', 'products', 'customers']
    
    for (const table of tables) {
      const { error } = await supabase
        .from(table)
        .delete()
        .neq('id', '00000000-0000-0000-0000-000000000000') // 保留系統數據
      
      if (error) {
        console.error(`清理 ${table} 失敗:`, error)
      } else {
        console.log(`✅ 已清理 ${table} 表格`)
      }
    }
    
    console.log('🎉 測試數據清理完成!')
  }
}

Vue 元件中的使用

vue
<template>
  <div class="faker-controls">
    <h3>測試數據生成工具</h3>
    
    <div class="control-group">
      <button @click="generateCustomers" :disabled="loading">
        生成客戶數據 ({{ customerCount }} 位)
      </button>
      <input v-model.number="customerCount" type="number" min="1" max="100" />
    </div>
    
    <div class="control-group">
      <button @click="generateProducts" :disabled="loading">
        生成商品數據 ({{ productCount }} 項)
      </button>
      <input v-model.number="productCount" type="number" min="1" max="500" />
    </div>
    
    <div class="control-group">
      <button @click="generateFullDataset" :disabled="loading" class="primary">
        🚀 生成完整數據集
      </button>
      <button @click="cleanAllData" :disabled="loading" class="danger">
        🧹 清理所有測試數據
      </button>
    </div>
    
    <div v-if="loading" class="loading">
      生成中... {{ progress }}%
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { BatchDataGenerator } from '@/api/seedFaker'

const loading = ref(false)
const progress = ref(0)
const customerCount = ref(20)
const productCount = ref(100)

const generateCustomers = async () => {
  loading.value = true
  try {
    const customers = BatchDataGenerator.generateCustomers(customerCount.value)
    await BatchDataGenerator.saveCustomers(customers)
  } finally {
    loading.value = false
  }
}

const generateFullDataset = async () => {
  loading.value = true
  try {
    await BatchDataGenerator.generateFullDataset()
  } finally {
    loading.value = false
  }
}

const cleanAllData = async () => {
  if (confirm('確定要清理所有測試數據?此操作無法復原!')) {
    loading.value = true
    try {
      await BatchDataGenerator.cleanTestData()
    } finally {
      loading.value = false
    }
  }
}
</script>

數據品質控制

數據驗證

typescript
interface DataValidation {
  validateCustomer(customer: Customer): boolean
  validateProduct(product: Product): boolean
  validateOrder(order: Order): boolean
}

export const dataValidator: DataValidation = {
  validateCustomer(customer) {
    return !!(
      customer.id &&
      customer.email &&
      customer.name &&
      customer.email.includes('@') &&
      customer.phone.match(/^09\d{8}$/)
    )
  },
  
  validateProduct(product) {
    return !!(
      product.id &&
      product.name &&
      product.category &&
      product.price.original > 0 &&
      product.inventory.quantity >= 0
    )
  },
  
  validateOrder(order) {
    return !!(
      order.id &&
      order.customerId &&
      order.items.length > 0 &&
      order.pricing.total > 0 &&
      ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'returned'].includes(order.status)
    )
  }
}

數據一致性檢查

typescript
export class DataConsistencyChecker {
  /**
   * 檢查訂單與客戶的一致性
   */
  static async checkOrderCustomerConsistency() {
    const { data: orders } = await supabase.from('orders').select('*')
    const { data: customers } = await supabase.from('customers').select('id')
    
    const customerIds = new Set(customers?.map(c => c.id))
    const inconsistentOrders = orders?.filter(order => 
      !customerIds.has(order.customerId)
    )
    
    if (inconsistentOrders?.length) {
      console.warn('發現不一致的訂單數據:', inconsistentOrders)
    }
    
    return inconsistentOrders || []
  }
}

高級功能

數據關聯性

typescript
// 確保生成的數據之間有正確的關聯性
class RelationalDataGenerator {
  static generateCustomerWithHistory(customerId: string) {
    // 先生成客戶基本資料
    const customer = generateCustomer()
    customer.id = customerId
    
    // 根據客戶等級生成對應的訂單歷史
    const orderCount = {
      'bronze': faker.number.int({ min: 1, max: 3 }),
      'silver': faker.number.int({ min: 3, max: 8 }),
      'gold': faker.number.int({ min: 8, max: 15 }),
      'platinum': faker.number.int({ min: 15, max: 30 })
    }[customer.tier]
    
    const orders = Array.from({ length: orderCount }, () => {
      const order = generateOrder(customerId)
      // 根據客戶等級調整訂單金額
      order.pricing.total *= {
        'bronze': 1,
        'silver': 1.2,
        'gold': 1.5,
        'platinum': 2
      }[customer.tier]
      
      return order
    })
    
    return { customer, orders }
  }
}

時間序列數據

typescript
// 生成具有時間序列特徵的數據
function generateTimeSeriesOrders(customerId: string, months: number = 12) {
  const orders: Order[] = []
  const baseDate = new Date()
  
  for (let month = 0; month < months; month++) {
    const monthDate = new Date(baseDate.getFullYear(), baseDate.getMonth() - month, 1)
    const ordersInMonth = faker.number.int({ min: 0, max: 4 })
    
    for (let i = 0; i < ordersInMonth; i++) {
      const order = generateOrder(customerId)
      order.createdAt = faker.date.between({
        from: monthDate,
        to: new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0)
      })
      orders.push(order)
    }
  }
  
  return orders.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
}

相關文檔


最後更新: $(date "+%Y-%m-%d")適用版本: front-stage-vue v1.0.0