10.1 项目需求分析

10.1.1 项目概述

我们将开发一个完整的电商平台,包含以下核心功能:

  • 商品展示与搜索
  • 用户注册与登录
  • 购物车管理
  • 订单处理
  • 支付集成
  • 管理后台

10.1.2 技术栈选择

// 技术栈配置
const techStack = {
  frontend: {
    framework: 'Vue.js 2.x',
    ssr: 'Vue Server Renderer',
    router: 'Vue Router',
    state: 'Vuex',
    ui: 'Element UI',
    build: 'Webpack 4',
    bundler: 'Vue CLI'
  },
  backend: {
    runtime: 'Node.js',
    framework: 'Express.js',
    database: 'MongoDB',
    orm: 'Mongoose',
    auth: 'JWT',
    cache: 'Redis',
    search: 'Elasticsearch'
  },
  devops: {
    containerization: 'Docker',
    orchestration: 'Docker Compose',
    ci_cd: 'GitHub Actions',
    monitoring: 'PM2',
    logging: 'Winston'
  }
}

10.1.3 项目架构设计

ecommerce-ssr/
├── client/                 # 客户端代码
│   ├── components/         # Vue组件
│   ├── pages/             # 页面组件
│   ├── store/             # Vuex状态管理
│   ├── router/            # 路由配置
│   ├── plugins/           # 插件
│   ├── utils/             # 工具函数
│   └── assets/            # 静态资源
├── server/                # 服务端代码
│   ├── api/               # API路由
│   ├── models/            # 数据模型
│   ├── middleware/        # 中间件
│   ├── services/          # 业务服务
│   ├── utils/             # 工具函数
│   └── config/            # 配置文件
├── shared/                # 共享代码
│   ├── constants/         # 常量定义
│   ├── utils/             # 通用工具
│   └── types/             # 类型定义
├── build/                 # 构建配置
├── static/                # 静态文件
├── tests/                 # 测试文件
└── docs/                  # 项目文档

10.2 项目初始化

10.2.1 项目脚手架搭建

# 创建项目目录
mkdir ecommerce-ssr
cd ecommerce-ssr

# 初始化package.json
npm init -y

# 安装核心依赖
npm install vue vue-server-renderer vue-router vuex express

# 安装开发依赖
npm install -D webpack webpack-cli webpack-dev-server
npm install -D vue-loader vue-template-compiler css-loader
npm install -D babel-loader @babel/core @babel/preset-env
npm install -D webpack-merge webpack-node-externals
npm install -D cross-env rimraf

# 安装UI库和工具
npm install element-ui axios lodash moment
npm install bcryptjs jsonwebtoken mongoose redis
npm install winston morgan helmet cors

# 安装测试工具
npm install -D jest @vue/test-utils supertest

10.2.2 Webpack配置

// build/webpack.base.config.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  devtool: isProd ? false : '#cheap-module-source-map',
  output: {
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/dist/',
    filename: '[name].[chunkhash].js'
  },
  resolve: {
    alias: {
      'public': path.resolve(__dirname, '../public'),
      '@': path.resolve(__dirname, '../client'),
      '~': path.resolve(__dirname, '../server'),
      'shared': path.resolve(__dirname, '../shared')
    },
    extensions: ['.js', '.vue', '.json']
  },
  module: {
    noParse: /es6-promise\.js$/, // avoid webpack shimming process
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            preserveWhitespace: false
          }
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: '[name].[ext]?[hash]'
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: 'fonts/[name].[hash:7].[ext]'
        }
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader']
      },
      {
        test: /\.scss$/,
        use: ['vue-style-loader', 'css-loader', 'sass-loader']
      }
    ]
  },
  performance: {
    maxEntrypointSize: 300000,
    hints: isProd ? 'warning' : false
  },
  plugins: isProd ? [
    new VueLoaderPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false }
    }),
    new webpack.optimize.ModuleConcatenationPlugin()
  ] : [
    new VueLoaderPlugin(),
    new FriendlyErrorsPlugin()
  ]
}
// build/webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
  entry: {
    app: './client/entry-client.js'
  },
  resolve: {
    alias: {
      'create-api': './create-api-client.js'
    }
  },
  plugins: [
    // strip dev-only code in Vue source
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"client"'
    }),
    // extract vendor chunks for better caching
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module) {
        // a module is extracted into the vendor chunk if...
        return (
          // it's inside node_modules
          /node_modules/.test(module.context) &&
          // and not a CSS file (due to extract-text-webpack-plugin limitation)
          !/\.css$/.test(module.request)
        )
      }
    }),
    // extract webpack runtime & manifest to avoid vendor chunk hash changing
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
    new VueSSRClientPlugin()
  ]
})

if (process.env.NODE_ENV === 'production') {
  config.plugins.push(
    // auto generate service worker
    new SWPrecachePlugin({
      cacheId: 'ecommerce-ssr',
      filename: 'service-worker.js',
      minify: true,
      dontCacheBustUrlsMatching: /./,
      staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/]
    })
  )
}

module.exports = config
// build/webpack.server.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const base = require('./webpack.base.config')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
  entry: './client/entry-server.js',
  target: 'node',
  devtool: '#source-map',
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    alias: {
      'create-api': './create-api-server.js'
    }
  },
  // https://webpack.js.org/configuration/externals/#externals
  // do not externalize CSS files in case we need to import it from a dep
  externals: nodeExternals({
    allowlist: /\.(css|vue)$/
  }),
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
  ]
})

10.2.3 项目配置文件

// server/config/index.js
const path = require('path')

module.exports = {
  // 服务器配置
  server: {
    port: process.env.PORT || 3000,
    host: process.env.HOST || 'localhost'
  },
  
  // 数据库配置
  database: {
    mongodb: {
      uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/ecommerce',
      options: {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true,
        useFindAndModify: false
      }
    },
    redis: {
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD || '',
      db: process.env.REDIS_DB || 0
    }
  },
  
  // JWT配置
  jwt: {
    secret: process.env.JWT_SECRET || 'your-secret-key',
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  },
  
  // 文件上传配置
  upload: {
    path: path.join(__dirname, '../../static/uploads'),
    maxSize: 5 * 1024 * 1024, // 5MB
    allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
  },
  
  // 邮件配置
  email: {
    service: process.env.EMAIL_SERVICE || 'gmail',
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  },
  
  // 支付配置
  payment: {
    stripe: {
      publicKey: process.env.STRIPE_PUBLIC_KEY,
      secretKey: process.env.STRIPE_SECRET_KEY
    },
    paypal: {
      clientId: process.env.PAYPAL_CLIENT_ID,
      clientSecret: process.env.PAYPAL_CLIENT_SECRET,
      mode: process.env.PAYPAL_MODE || 'sandbox'
    }
  },
  
  // 缓存配置
  cache: {
    ttl: {
      page: 15 * 60, // 15分钟
      api: 5 * 60,   // 5分钟
      static: 24 * 60 * 60 // 24小时
    }
  },
  
  // 日志配置
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    file: process.env.LOG_FILE || 'logs/app.log'
  }
}

10.3 核心功能实现

10.3.1 用户认证系统

// server/models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const config = require('../config')

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3,
    maxlength: 30
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  profile: {
    firstName: String,
    lastName: String,
    avatar: String,
    phone: String,
    dateOfBirth: Date
  },
  addresses: [{
    type: {
      type: String,
      enum: ['home', 'work', 'other'],
      default: 'home'
    },
    street: String,
    city: String,
    state: String,
    zipCode: String,
    country: String,
    isDefault: {
      type: Boolean,
      default: false
    }
  }],
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  isActive: {
    type: Boolean,
    default: true
  },
  emailVerified: {
    type: Boolean,
    default: false
  },
  lastLogin: Date,
  preferences: {
    language: {
      type: String,
      default: 'en'
    },
    currency: {
      type: String,
      default: 'USD'
    },
    notifications: {
      email: {
        type: Boolean,
        default: true
      },
      sms: {
        type: Boolean,
        default: false
      }
    }
  }
}, {
  timestamps: true
})

// 密码加密中间件
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next()
  
  try {
    const salt = await bcrypt.genSalt(12)
    this.password = await bcrypt.hash(this.password, salt)
    next()
  } catch (error) {
    next(error)
  }
})

// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password)
}

// 生成JWT令牌
userSchema.methods.generateAuthToken = function() {
  const payload = {
    id: this._id,
    username: this.username,
    email: this.email,
    role: this.role
  }
  
  return jwt.sign(payload, config.jwt.secret, {
    expiresIn: config.jwt.expiresIn
  })
}

// 获取公开信息
userSchema.methods.toPublicJSON = function() {
  return {
    id: this._id,
    username: this.username,
    email: this.email,
    profile: this.profile,
    role: this.role,
    isActive: this.isActive,
    emailVerified: this.emailVerified,
    preferences: this.preferences,
    createdAt: this.createdAt
  }
}

// 索引
userSchema.index({ email: 1 })
userSchema.index({ username: 1 })
userSchema.index({ 'addresses.isDefault': 1 })

module.exports = mongoose.model('User', userSchema)
// server/services/AuthService.js
const User = require('../models/User')
const jwt = require('jsonwebtoken')
const config = require('../config')
const { AppError } = require('../utils/errors')

class AuthService {
  // 用户注册
  async register(userData) {
    const { username, email, password } = userData
    
    // 检查用户是否已存在
    const existingUser = await User.findOne({
      $or: [{ email }, { username }]
    })
    
    if (existingUser) {
      throw new AppError('用户已存在', 400)
    }
    
    // 创建新用户
    const user = new User({
      username,
      email,
      password,
      profile: userData.profile || {}
    })
    
    await user.save()
    
    // 生成令牌
    const token = user.generateAuthToken()
    
    return {
      user: user.toPublicJSON(),
      token
    }
  }
  
  // 用户登录
  async login(credentials) {
    const { email, password } = credentials
    
    // 查找用户
    const user = await User.findOne({ email })
    if (!user) {
      throw new AppError('邮箱或密码错误', 401)
    }
    
    // 验证密码
    const isPasswordValid = await user.comparePassword(password)
    if (!isPasswordValid) {
      throw new AppError('邮箱或密码错误', 401)
    }
    
    // 检查账户状态
    if (!user.isActive) {
      throw new AppError('账户已被禁用', 401)
    }
    
    // 更新最后登录时间
    user.lastLogin = new Date()
    await user.save()
    
    // 生成令牌
    const token = user.generateAuthToken()
    
    return {
      user: user.toPublicJSON(),
      token
    }
  }
  
  // 验证令牌
  async verifyToken(token) {
    try {
      const decoded = jwt.verify(token, config.jwt.secret)
      const user = await User.findById(decoded.id)
      
      if (!user || !user.isActive) {
        throw new AppError('无效的令牌', 401)
      }
      
      return user
    } catch (error) {
      if (error.name === 'JsonWebTokenError') {
        throw new AppError('无效的令牌', 401)
      }
      if (error.name === 'TokenExpiredError') {
        throw new AppError('令牌已过期', 401)
      }
      throw error
    }
  }
  
  // 刷新令牌
  async refreshToken(token) {
    const user = await this.verifyToken(token)
    return user.generateAuthToken()
  }
  
  // 修改密码
  async changePassword(userId, oldPassword, newPassword) {
    const user = await User.findById(userId)
    if (!user) {
      throw new AppError('用户不存在', 404)
    }
    
    // 验证旧密码
    const isOldPasswordValid = await user.comparePassword(oldPassword)
    if (!isOldPasswordValid) {
      throw new AppError('原密码错误', 400)
    }
    
    // 更新密码
    user.password = newPassword
    await user.save()
    
    return { message: '密码修改成功' }
  }
  
  // 重置密码
  async resetPassword(email) {
    const user = await User.findOne({ email })
    if (!user) {
      throw new AppError('用户不存在', 404)
    }
    
    // 生成重置令牌
    const resetToken = jwt.sign(
      { id: user._id, type: 'password_reset' },
      config.jwt.secret,
      { expiresIn: '1h' }
    )
    
    // 这里应该发送邮件
    // await emailService.sendPasswordResetEmail(user.email, resetToken)
    
    return { message: '重置密码邮件已发送' }
  }
  
  // 确认重置密码
  async confirmPasswordReset(token, newPassword) {
    try {
      const decoded = jwt.verify(token, config.jwt.secret)
      
      if (decoded.type !== 'password_reset') {
        throw new AppError('无效的重置令牌', 400)
      }
      
      const user = await User.findById(decoded.id)
      if (!user) {
        throw new AppError('用户不存在', 404)
      }
      
      user.password = newPassword
      await user.save()
      
      return { message: '密码重置成功' }
    } catch (error) {
      if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
        throw new AppError('无效或过期的重置令牌', 400)
      }
      throw error
    }
  }
}

module.exports = new AuthService()

10.3.2 商品管理系统

// server/models/Product.js
const mongoose = require('mongoose')

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true,
    maxlength: 200
  },
  slug: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  description: {
    type: String,
    required: true,
    maxlength: 2000
  },
  shortDescription: {
    type: String,
    maxlength: 500
  },
  sku: {
    type: String,
    required: true,
    unique: true
  },
  price: {
    regular: {
      type: Number,
      required: true,
      min: 0
    },
    sale: {
      type: Number,
      min: 0
    },
    currency: {
      type: String,
      default: 'USD'
    }
  },
  inventory: {
    quantity: {
      type: Number,
      required: true,
      min: 0,
      default: 0
    },
    trackQuantity: {
      type: Boolean,
      default: true
    },
    allowBackorder: {
      type: Boolean,
      default: false
    }
  },
  images: [{
    url: {
      type: String,
      required: true
    },
    alt: String,
    isPrimary: {
      type: Boolean,
      default: false
    }
  }],
  categories: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Category'
  }],
  tags: [String],
  attributes: [{
    name: {
      type: String,
      required: true
    },
    value: {
      type: String,
      required: true
    },
    type: {
      type: String,
      enum: ['text', 'number', 'boolean', 'select'],
      default: 'text'
    }
  }],
  variants: [{
    name: String,
    sku: String,
    price: Number,
    inventory: Number,
    attributes: [{
      name: String,
      value: String
    }],
    images: [String]
  }],
  seo: {
    title: String,
    description: String,
    keywords: [String]
  },
  status: {
    type: String,
    enum: ['draft', 'published', 'archived'],
    default: 'draft'
  },
  featured: {
    type: Boolean,
    default: false
  },
  weight: {
    value: Number,
    unit: {
      type: String,
      enum: ['g', 'kg', 'lb', 'oz'],
      default: 'kg'
    }
  },
  dimensions: {
    length: Number,
    width: Number,
    height: Number,
    unit: {
      type: String,
      enum: ['cm', 'm', 'in', 'ft'],
      default: 'cm'
    }
  },
  shipping: {
    free: {
      type: Boolean,
      default: false
    },
    weight: Number,
    class: String
  },
  reviews: {
    count: {
      type: Number,
      default: 0
    },
    average: {
      type: Number,
      default: 0,
      min: 0,
      max: 5
    }
  },
  sales: {
    count: {
      type: Number,
      default: 0
    },
    revenue: {
      type: Number,
      default: 0
    }
  },
  createdBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  updatedBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }
}, {
  timestamps: true
})

// 虚拟字段:当前价格
productSchema.virtual('currentPrice').get(function() {
  return this.price.sale || this.price.regular
})

// 虚拟字段:是否有折扣
productSchema.virtual('onSale').get(function() {
  return this.price.sale && this.price.sale < this.price.regular
})

// 虚拟字段:折扣百分比
productSchema.virtual('discountPercentage').get(function() {
  if (!this.onSale) return 0
  return Math.round(((this.price.regular - this.price.sale) / this.price.regular) * 100)
})

// 虚拟字段:是否有库存
productSchema.virtual('inStock').get(function() {
  if (!this.inventory.trackQuantity) return true
  return this.inventory.quantity > 0 || this.inventory.allowBackorder
})

// 中间件:保存前生成slug
productSchema.pre('save', function(next) {
  if (this.isModified('name') && !this.slug) {
    this.slug = this.name
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
  }
  next()
})

// 中间件:确保只有一个主图片
productSchema.pre('save', function(next) {
  if (this.images && this.images.length > 0) {
    let primaryCount = 0
    this.images.forEach((image, index) => {
      if (image.isPrimary) {
        primaryCount++
        if (primaryCount > 1) {
          this.images[index].isPrimary = false
        }
      }
    })
    
    // 如果没有主图片,设置第一张为主图片
    if (primaryCount === 0) {
      this.images[0].isPrimary = true
    }
  }
  next()
})

// 静态方法:搜索产品
productSchema.statics.search = function(query, options = {}) {
  const {
    page = 1,
    limit = 20,
    sort = '-createdAt',
    category,
    minPrice,
    maxPrice,
    inStock,
    featured
  } = options
  
  const filter = {
    status: 'published'
  }
  
  // 文本搜索
  if (query) {
    filter.$or = [
      { name: { $regex: query, $options: 'i' } },
      { description: { $regex: query, $options: 'i' } },
      { tags: { $in: [new RegExp(query, 'i')] } }
    ]
  }
  
  // 分类过滤
  if (category) {
    filter.categories = category
  }
  
  // 价格过滤
  if (minPrice || maxPrice) {
    filter['price.regular'] = {}
    if (minPrice) filter['price.regular'].$gte = minPrice
    if (maxPrice) filter['price.regular'].$lte = maxPrice
  }
  
  // 库存过滤
  if (inStock) {
    filter.$or = [
      { 'inventory.trackQuantity': false },
      { 'inventory.quantity': { $gt: 0 } },
      { 'inventory.allowBackorder': true }
    ]
  }
  
  // 特色产品过滤
  if (featured) {
    filter.featured = true
  }
  
  const skip = (page - 1) * limit
  
  return this.find(filter)
    .populate('categories', 'name slug')
    .sort(sort)
    .skip(skip)
    .limit(limit)
}

// 实例方法:更新库存
productSchema.methods.updateInventory = function(quantity, operation = 'subtract') {
  if (!this.inventory.trackQuantity) return this
  
  if (operation === 'subtract') {
    this.inventory.quantity = Math.max(0, this.inventory.quantity - quantity)
  } else if (operation === 'add') {
    this.inventory.quantity += quantity
  }
  
  return this.save()
}

// 实例方法:添加评论统计
productSchema.methods.updateReviewStats = function(rating) {
  const totalRating = (this.reviews.average * this.reviews.count) + rating
  this.reviews.count += 1
  this.reviews.average = totalRating / this.reviews.count
  
  return this.save()
}

// 索引
productSchema.index({ name: 'text', description: 'text', tags: 'text' })
productSchema.index({ slug: 1 })
productSchema.index({ sku: 1 })
productSchema.index({ categories: 1 })
productSchema.index({ status: 1 })
productSchema.index({ featured: 1 })
productSchema.index({ 'price.regular': 1 })
productSchema.index({ createdAt: -1 })
productSchema.index({ 'reviews.average': -1 })
productSchema.index({ 'sales.count': -1 })

module.exports = mongoose.model('Product', productSchema)

10.3.3 购物车系统

// client/store/modules/cart.js
const state = {
  items: [],
  loading: false,
  error: null
}

const getters = {
  cartItems: state => state.items,
  cartTotal: state => {
    return state.items.reduce((total, item) => {
      return total + (item.price * item.quantity)
    }, 0)
  },
  cartCount: state => {
    return state.items.reduce((count, item) => count + item.quantity, 0)
  },
  isInCart: state => productId => {
    return state.items.some(item => item.productId === productId)
  },
  getCartItem: state => productId => {
    return state.items.find(item => item.productId === productId)
  }
}

const mutations = {
  SET_LOADING(state, loading) {
    state.loading = loading
  },
  SET_ERROR(state, error) {
    state.error = error
  },
  SET_CART_ITEMS(state, items) {
    state.items = items
  },
  ADD_TO_CART(state, item) {
    const existingItem = state.items.find(i => i.productId === item.productId)
    
    if (existingItem) {
      existingItem.quantity += item.quantity
    } else {
      state.items.push({
        ...item,
        id: Date.now() + Math.random() // 临时ID
      })
    }
  },
  UPDATE_CART_ITEM(state, { productId, quantity }) {
    const item = state.items.find(i => i.productId === productId)
    if (item) {
      if (quantity <= 0) {
        state.items = state.items.filter(i => i.productId !== productId)
      } else {
        item.quantity = quantity
      }
    }
  },
  REMOVE_FROM_CART(state, productId) {
    state.items = state.items.filter(item => item.productId !== productId)
  },
  CLEAR_CART(state) {
    state.items = []
  }
}

const actions = {
  // 加载购物车
  async loadCart({ commit }) {
    commit('SET_LOADING', true)
    commit('SET_ERROR', null)
    
    try {
      // 从localStorage加载(客户端)
      if (process.client) {
        const cartData = localStorage.getItem('cart')
        if (cartData) {
          const items = JSON.parse(cartData)
          commit('SET_CART_ITEMS', items)
        }
      }
      
      // 如果用户已登录,从服务器同步
      if (this.getters['auth/isAuthenticated']) {
        const response = await this.$api.get('/cart')
        commit('SET_CART_ITEMS', response.data.items)
        
        // 同步到localStorage
        if (process.client) {
          localStorage.setItem('cart', JSON.stringify(response.data.items))
        }
      }
    } catch (error) {
      commit('SET_ERROR', error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  },
  
  // 添加到购物车
  async addToCart({ commit, dispatch, getters }, { product, quantity = 1, variant = null }) {
    commit('SET_LOADING', true)
    commit('SET_ERROR', null)
    
    try {
      const cartItem = {
        productId: product.id,
        name: product.name,
        slug: product.slug,
        price: product.currentPrice,
        image: product.images[0]?.url,
        quantity,
        variant
      }
      
      // 检查库存
      if (product.inventory.trackQuantity) {
        const currentItem = getters.getCartItem(product.id)
        const totalQuantity = (currentItem?.quantity || 0) + quantity
        
        if (totalQuantity > product.inventory.quantity && !product.inventory.allowBackorder) {
          throw new Error('库存不足')
        }
      }
      
      commit('ADD_TO_CART', cartItem)
      
      // 保存到localStorage
      if (process.client) {
        localStorage.setItem('cart', JSON.stringify(getters.cartItems))
      }
      
      // 如果用户已登录,同步到服务器
      if (this.getters['auth/isAuthenticated']) {
        await this.$api.post('/cart/add', cartItem)
      }
      
      // 显示成功消息
      this.$toast.success('商品已添加到购物车')
      
    } catch (error) {
      commit('SET_ERROR', error.message)
      this.$toast.error(error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  },
  
  // 更新购物车项目
  async updateCartItem({ commit, getters }, { productId, quantity }) {
    commit('SET_LOADING', true)
    commit('SET_ERROR', null)
    
    try {
      commit('UPDATE_CART_ITEM', { productId, quantity })
      
      // 保存到localStorage
      if (process.client) {
        localStorage.setItem('cart', JSON.stringify(getters.cartItems))
      }
      
      // 如果用户已登录,同步到服务器
      if (this.getters['auth/isAuthenticated']) {
        await this.$api.put(`/cart/items/${productId}`, { quantity })
      }
      
    } catch (error) {
      commit('SET_ERROR', error.message)
      this.$toast.error(error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  },
  
  // 从购物车移除
  async removeFromCart({ commit, getters }, productId) {
    commit('SET_LOADING', true)
    commit('SET_ERROR', null)
    
    try {
      commit('REMOVE_FROM_CART', productId)
      
      // 保存到localStorage
      if (process.client) {
        localStorage.setItem('cart', JSON.stringify(getters.cartItems))
      }
      
      // 如果用户已登录,同步到服务器
      if (this.getters['auth/isAuthenticated']) {
        await this.$api.delete(`/cart/items/${productId}`)
      }
      
      this.$toast.success('商品已从购物车移除')
      
    } catch (error) {
      commit('SET_ERROR', error.message)
      this.$toast.error(error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  },
  
  // 清空购物车
  async clearCart({ commit }) {
    commit('SET_LOADING', true)
    commit('SET_ERROR', null)
    
    try {
      commit('CLEAR_CART')
      
      // 清除localStorage
      if (process.client) {
        localStorage.removeItem('cart')
      }
      
      // 如果用户已登录,清除服务器购物车
      if (this.getters['auth/isAuthenticated']) {
        await this.$api.delete('/cart')
      }
      
      this.$toast.success('购物车已清空')
      
    } catch (error) {
      commit('SET_ERROR', error.message)
      this.$toast.error(error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  },
  
  // 同步购物车(登录后)
  async syncCart({ commit, getters }) {
    if (!this.getters['auth/isAuthenticated']) return
    
    try {
      const localItems = getters.cartItems
      
      if (localItems.length > 0) {
        // 将本地购物车同步到服务器
        await this.$api.post('/cart/sync', { items: localItems })
      }
      
      // 从服务器获取最新购物车
      const response = await this.$api.get('/cart')
      commit('SET_CART_ITEMS', response.data.items)
      
      // 更新localStorage
      if (process.client) {
        localStorage.setItem('cart', JSON.stringify(response.data.items))
      }
      
    } catch (error) {
      console.error('Cart sync error:', error)
    }
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

10.4 本章小结

本章介绍了Vue SSR实战项目的开发过程,包括:

核心要点

  1. 项目架构设计

    • 技术栈选择与配置
    • 项目结构规划
    • 开发环境搭建
  2. 核心功能实现

    • 用户认证系统
    • 商品管理系统
    • 购物车功能
  3. 数据模型设计

    • MongoDB数据建模
    • 索引优化
    • 关联关系处理

最佳实践

  1. 代码组织

    • 模块化设计
    • 分层架构
    • 代码复用
  2. 状态管理

    • Vuex模块化
    • 客户端与服务端同步
    • 本地存储集成
  3. 错误处理

    • 统一错误处理
    • 用户友好提示
    • 日志记录

练习作业

  1. 完善用户认证功能
  2. 实现商品搜索与过滤
  3. 开发订单管理系统
  4. 集成支付功能

下一章我们将学习Vue SSR的生态系统与工具链。