Skip to content

Feathers.js + Sequelize 实战项目 - 构建完整的电商系统

发布时间:2024-08-05
作者:一介布衣
标签:Feathers.js, Sequelize, 电商系统, 实战项目, 完整案例

前言

前面三篇文章我们深入学习了 Feathers.js 与 Sequelize 的各个方面,今天咱们来做一个完整的实战项目 - 电商系统。说实话,电商系统是最能体现关系型数据库优势的项目之一,涉及用户、商品、订单、支付、库存等复杂的业务逻辑和数据关系。

我记得第一次做电商项目的时候,被各种业务规则搞得头大:库存扣减、订单状态流转、支付回调处理、优惠券计算等等。后来用了 Sequelize 的事务和钩子系统,发现原来可以这么优雅地处理复杂的业务逻辑。

今天我就带大家从零开始,构建一个功能完整的电商系统,涵盖商品管理、购物车、订单处理、支付集成等核心功能。

项目架构设计

核心功能模块

用户系统

  • 用户注册登录
  • 个人信息管理
  • 收货地址管理
  • 用户等级和积分

商品系统

  • 商品分类管理
  • 商品信息管理
  • 库存管理
  • 商品评价系统

订单系统

  • 购物车管理
  • 订单创建和管理
  • 订单状态流转
  • 退款退货处理

支付系统

  • 多种支付方式
  • 支付回调处理
  • 退款处理
  • 账单管理

营销系统

  • 优惠券管理
  • 促销活动
  • 积分系统
  • 推荐系统

数据模型设计

1. 用户相关模型

javascript
// src/models/users.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const users = sequelizeClient.define('users', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    // 基本信息
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
      validate: { isEmail: true }
    },
    
    phone: {
      type: DataTypes.STRING,
      unique: true,
      validate: {
        is: /^1[3-9]\d{9}$/
      }
    },
    
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    nickname: DataTypes.STRING,
    avatar: DataTypes.STRING,
    gender: {
      type: DataTypes.ENUM('male', 'female', 'unknown'),
      defaultValue: 'unknown'
    },
    
    birthDate: {
      type: DataTypes.DATEONLY,
      field: 'birth_date'
    },
    
    // 用户等级和积分
    level: {
      type: DataTypes.INTEGER,
      defaultValue: 1
    },
    
    points: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    totalSpent: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'total_spent'
    },
    
    // 状态
    status: {
      type: DataTypes.ENUM('active', 'inactive', 'banned'),
      defaultValue: 'active'
    },
    
    emailVerified: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'email_verified'
    },
    
    phoneVerified: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'phone_verified'
    },
    
    // 偏好设置
    preferences: {
      type: DataTypes.JSON,
      defaultValue: {
        notifications: {
          order: true,
          promotion: true,
          system: true
        },
        privacy: {
          showProfile: true,
          showPurchaseHistory: false
        }
      }
    },
    
    lastLoginAt: {
      type: DataTypes.DATE,
      field: 'last_login_at'
    }
  }, {
    tableName: 'users',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['email'] },
      { fields: ['phone'] },
      { fields: ['status'] },
      { fields: ['level'] }
    ]
  });

  users.associate = function(models) {
    // 用户地址
    users.hasMany(models.userAddresses, {
      foreignKey: 'userId',
      as: 'addresses'
    });
    
    // 用户订单
    users.hasMany(models.orders, {
      foreignKey: 'userId',
      as: 'orders'
    });
    
    // 购物车
    users.hasMany(models.cartItems, {
      foreignKey: 'userId',
      as: 'cartItems'
    });
    
    // 商品收藏
    users.belongsToMany(models.products, {
      through: 'user_favorites',
      foreignKey: 'userId',
      otherKey: 'productId',
      as: 'favorites'
    });
  };

  return users;
};

// src/models/user-addresses.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const userAddresses = sequelizeClient.define('userAddresses', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    userId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'user_id'
    },
    
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    phone: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    province: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    city: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    district: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    address: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    postalCode: {
      type: DataTypes.STRING,
      field: 'postal_code'
    },
    
    isDefault: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'is_default'
    }
  }, {
    tableName: 'user_addresses',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['user_id'] },
      { fields: ['is_default'] }
    ]
  });

  userAddresses.associate = function(models) {
    userAddresses.belongsTo(models.users, {
      foreignKey: 'userId',
      as: 'user'
    });
  };

  return userAddresses;
};

2. 商品相关模型

javascript
// src/models/categories.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const categories = sequelizeClient.define('categories', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    slug: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    description: DataTypes.TEXT,
    image: DataTypes.STRING,
    icon: DataTypes.STRING,
    
    // 层级结构
    parentId: {
      type: DataTypes.UUID,
      field: 'parent_id'
    },
    
    level: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    path: DataTypes.STRING,
    
    // 排序和状态
    sortOrder: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'sort_order'
    },
    
    isActive: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
      field: 'is_active'
    },
    
    // 统计
    productsCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'products_count'
    }
  }, {
    tableName: 'categories',
    timestamps: true,
    underscored: true
  });

  categories.associate = function(models) {
    // 分类商品
    categories.hasMany(models.products, {
      foreignKey: 'categoryId',
      as: 'products'
    });
    
    // 自关联
    categories.hasMany(categories, {
      foreignKey: 'parentId',
      as: 'children'
    });
    
    categories.belongsTo(categories, {
      foreignKey: 'parentId',
      as: 'parent'
    });
  };

  return categories;
};

// src/models/products.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const products = sequelizeClient.define('products', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    // 基本信息
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    slug: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    description: DataTypes.TEXT,
    shortDescription: {
      type: DataTypes.TEXT,
      field: 'short_description'
    },
    
    // 分类
    categoryId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'category_id'
    },
    
    // 品牌
    brand: DataTypes.STRING,
    model: DataTypes.STRING,
    
    // 价格
    price: {
      type: DataTypes.DECIMAL(10, 2),
      allowNull: false
    },
    
    originalPrice: {
      type: DataTypes.DECIMAL(10, 2),
      field: 'original_price'
    },
    
    costPrice: {
      type: DataTypes.DECIMAL(10, 2),
      field: 'cost_price'
    },
    
    // 库存
    stock: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    minStock: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'min_stock'
    },
    
    maxStock: {
      type: DataTypes.INTEGER,
      field: 'max_stock'
    },
    
    // 规格属性
    attributes: {
      type: DataTypes.JSON,
      defaultValue: {}
    },
    
    // 媒体
    images: {
      type: DataTypes.JSON,
      defaultValue: []
    },
    
    videos: {
      type: DataTypes.JSON,
      defaultValue: []
    },
    
    // 重量和尺寸
    weight: DataTypes.DECIMAL(8, 2),
    length: DataTypes.DECIMAL(8, 2),
    width: DataTypes.DECIMAL(8, 2),
    height: DataTypes.DECIMAL(8, 2),
    
    // 状态
    status: {
      type: DataTypes.ENUM('active', 'inactive', 'out_of_stock'),
      defaultValue: 'active'
    },
    
    isFeatured: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'is_featured'
    },
    
    isDigital: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'is_digital'
    },
    
    // SEO
    metaTitle: {
      type: DataTypes.STRING,
      field: 'meta_title'
    },
    
    metaDescription: {
      type: DataTypes.TEXT,
      field: 'meta_description'
    },
    
    metaKeywords: {
      type: DataTypes.JSON,
      field: 'meta_keywords',
      defaultValue: []
    },
    
    // 统计
    viewCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'view_count'
    },
    
    salesCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'sales_count'
    },
    
    favoriteCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'favorite_count'
    },
    
    rating: {
      type: DataTypes.DECIMAL(3, 2),
      defaultValue: 0
    },
    
    reviewCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'review_count'
    }
  }, {
    tableName: 'products',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['category_id'] },
      { fields: ['slug'] },
      { fields: ['status'] },
      { fields: ['is_featured'] },
      { fields: ['price'] },
      { fields: ['stock'] },
      { fields: ['brand'] }
    ]
  });

  products.associate = function(models) {
    // 商品分类
    products.belongsTo(models.categories, {
      foreignKey: 'categoryId',
      as: 'category'
    });
    
    // 商品变体
    products.hasMany(models.productVariants, {
      foreignKey: 'productId',
      as: 'variants'
    });
    
    // 商品评价
    products.hasMany(models.productReviews, {
      foreignKey: 'productId',
      as: 'reviews'
    });
    
    // 购物车项目
    products.hasMany(models.cartItems, {
      foreignKey: 'productId',
      as: 'cartItems'
    });
    
    // 订单项目
    products.hasMany(models.orderItems, {
      foreignKey: 'productId',
      as: 'orderItems'
    });
  };

  return products;
};

// src/models/product-variants.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const productVariants = sequelizeClient.define('productVariants', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    productId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'product_id'
    },
    
    sku: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    name: DataTypes.STRING,
    
    // 变体属性(如:颜色、尺寸等)
    attributes: {
      type: DataTypes.JSON,
      allowNull: false
    },
    
    // 价格(可以覆盖主商品价格)
    price: DataTypes.DECIMAL(10, 2),
    originalPrice: {
      type: DataTypes.DECIMAL(10, 2),
      field: 'original_price'
    },
    
    // 库存
    stock: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    // 媒体
    images: {
      type: DataTypes.JSON,
      defaultValue: []
    },
    
    // 重量和尺寸(可以覆盖主商品)
    weight: DataTypes.DECIMAL(8, 2),
    length: DataTypes.DECIMAL(8, 2),
    width: DataTypes.DECIMAL(8, 2),
    height: DataTypes.DECIMAL(8, 2),
    
    // 状态
    isActive: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
      field: 'is_active'
    },
    
    sortOrder: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'sort_order'
    }
  }, {
    tableName: 'product_variants',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['product_id'] },
      { fields: ['sku'] },
      { fields: ['is_active'] }
    ]
  });

  productVariants.associate = function(models) {
    productVariants.belongsTo(models.products, {
      foreignKey: 'productId',
      as: 'product'
    });
  };

  return productVariants;
};

3. 订单相关模型

javascript
// src/models/orders.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const orders = sequelizeClient.define('orders', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    // 订单号
    orderNumber: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
      field: 'order_number'
    },
    
    // 用户信息
    userId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'user_id'
    },
    
    // 收货信息
    shippingAddress: {
      type: DataTypes.JSON,
      allowNull: false,
      field: 'shipping_address'
    },
    
    billingAddress: {
      type: DataTypes.JSON,
      field: 'billing_address'
    },
    
    // 金额信息
    subtotal: {
      type: DataTypes.DECIMAL(10, 2),
      allowNull: false
    },
    
    shippingFee: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'shipping_fee'
    },
    
    taxAmount: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'tax_amount'
    },
    
    discountAmount: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'discount_amount'
    },
    
    totalAmount: {
      type: DataTypes.DECIMAL(10, 2),
      allowNull: false,
      field: 'total_amount'
    },
    
    // 优惠券
    couponId: {
      type: DataTypes.UUID,
      field: 'coupon_id'
    },
    
    couponDiscount: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'coupon_discount'
    },
    
    // 积分
    pointsUsed: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'points_used'
    },
    
    pointsEarned: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'points_earned'
    },
    
    // 状态
    status: {
      type: DataTypes.ENUM(
        'pending',      // 待付款
        'paid',         // 已付款
        'processing',   // 处理中
        'shipped',      // 已发货
        'delivered',    // 已送达
        'completed',    // 已完成
        'cancelled',    // 已取消
        'refunded'      // 已退款
      ),
      defaultValue: 'pending'
    },
    
    paymentStatus: {
      type: DataTypes.ENUM('pending', 'paid', 'failed', 'refunded'),
      defaultValue: 'pending',
      field: 'payment_status'
    },
    
    shippingStatus: {
      type: DataTypes.ENUM('pending', 'processing', 'shipped', 'delivered'),
      defaultValue: 'pending',
      field: 'shipping_status'
    },
    
    // 时间信息
    paidAt: {
      type: DataTypes.DATE,
      field: 'paid_at'
    },
    
    shippedAt: {
      type: DataTypes.DATE,
      field: 'shipped_at'
    },
    
    deliveredAt: {
      type: DataTypes.DATE,
      field: 'delivered_at'
    },
    
    // 备注
    notes: DataTypes.TEXT,
    adminNotes: {
      type: DataTypes.TEXT,
      field: 'admin_notes'
    },
    
    // 物流信息
    shippingMethod: {
      type: DataTypes.STRING,
      field: 'shipping_method'
    },
    
    trackingNumber: {
      type: DataTypes.STRING,
      field: 'tracking_number'
    },
    
    // 发票信息
    invoiceRequired: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'invoice_required'
    },
    
    invoiceInfo: {
      type: DataTypes.JSON,
      field: 'invoice_info'
    }
  }, {
    tableName: 'orders',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['user_id'] },
      { fields: ['order_number'] },
      { fields: ['status'] },
      { fields: ['payment_status'] },
      { fields: ['created_at'] }
    ]
  });

  orders.associate = function(models) {
    // 订单用户
    orders.belongsTo(models.users, {
      foreignKey: 'userId',
      as: 'user'
    });
    
    // 订单项目
    orders.hasMany(models.orderItems, {
      foreignKey: 'orderId',
      as: 'items'
    });
    
    // 支付记录
    orders.hasMany(models.payments, {
      foreignKey: 'orderId',
      as: 'payments'
    });
    
    // 退款记录
    orders.hasMany(models.refunds, {
      foreignKey: 'orderId',
      as: 'refunds'
    });
    
    // 优惠券
    orders.belongsTo(models.coupons, {
      foreignKey: 'couponId',
      as: 'coupon'
    });
  };

  // 实例方法
  orders.prototype.canCancel = function() {
    return ['pending', 'paid'].includes(this.status);
  };

  orders.prototype.canRefund = function() {
    return ['paid', 'processing', 'shipped', 'delivered'].includes(this.status);
  };

  orders.prototype.getTotalWeight = async function() {
    const items = await this.getItems({
      include: ['product', 'variant']
    });
    
    return items.reduce((total, item) => {
      const weight = item.variant?.weight || item.product.weight || 0;
      return total + (weight * item.quantity);
    }, 0);
  };

  // 类方法
  orders.generateOrderNumber = function() {
    const now = new Date();
    const year = now.getFullYear().toString().slice(-2);
    const month = (now.getMonth() + 1).toString().padStart(2, '0');
    const day = now.getDate().toString().padStart(2, '0');
    const random = Math.random().toString(36).substr(2, 6).toUpperCase();
    
    return `\${year}\${month}\${day}\${random}`;
  };

  return orders;
};

// src/models/order-items.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const orderItems = sequelizeClient.define('orderItems', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    orderId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'order_id'
    },
    
    productId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'product_id'
    },
    
    variantId: {
      type: DataTypes.UUID,
      field: 'variant_id'
    },
    
    // 商品信息快照
    productName: {
      type: DataTypes.STRING,
      allowNull: false,
      field: 'product_name'
    },
    
    productImage: {
      type: DataTypes.STRING,
      field: 'product_image'
    },
    
    variantAttributes: {
      type: DataTypes.JSON,
      field: 'variant_attributes'
    },
    
    // 价格和数量
    price: {
      type: DataTypes.DECIMAL(10, 2),
      allowNull: false
    },
    
    quantity: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    
    total: {
      type: DataTypes.DECIMAL(10, 2),
      allowNull: false
    },
    
    // 退款状态
    refundStatus: {
      type: DataTypes.ENUM('none', 'partial', 'full'),
      defaultValue: 'none',
      field: 'refund_status'
    },
    
    refundedQuantity: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'refunded_quantity'
    },
    
    refundedAmount: {
      type: DataTypes.DECIMAL(10, 2),
      defaultValue: 0,
      field: 'refunded_amount'
    }
  }, {
    tableName: 'order_items',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['order_id'] },
      { fields: ['product_id'] },
      { fields: ['variant_id'] }
    ]
  });

  orderItems.associate = function(models) {
    orderItems.belongsTo(models.orders, {
      foreignKey: 'orderId',
      as: 'order'
    });
    
    orderItems.belongsTo(models.products, {
      foreignKey: 'productId',
      as: 'product'
    });
    
    orderItems.belongsTo(models.productVariants, {
      foreignKey: 'variantId',
      as: 'variant'
    });
  };

  return orderItems;
};

核心业务服务

1. 购物车服务

javascript
// src/services/cart/cart.class.js
const { Service } = require('feathers-sequelize');

class CartService extends Service {
  constructor(options, app) {
    super(options, app);
    this.app = app;
  }

  // 添加商品到购物车
  async addToCart(data, params) {
    const { user } = params;
    const { productId, variantId, quantity = 1 } = data;

    return this.Model.sequelize.transaction(async (t) => {
      // 检查商品和变体
      const product = await this.app.service('products').get(productId, {
        query: { $populate: ['variants'] },
        transaction: t
      });

      if (product.status !== 'active') {
        throw new Error('商品已下架');
      }

      let variant = null;
      let availableStock = product.stock;
      let price = product.price;

      if (variantId) {
        variant = product.variants.find(v => v.id === variantId);
        if (!variant || !variant.isActive) {
          throw new Error('商品规格不存在或已下架');
        }
        availableStock = variant.stock;
        price = variant.price || product.price;
      }

      if (availableStock < quantity) {
        throw new Error('库存不足');
      }

      // 检查购物车中是否已有相同商品
      const existingItem = await this.Model.findOne({
        where: {
          userId: user.id,
          productId,
          variantId: variantId || null
        },
        transaction: t
      });

      if (existingItem) {
        // 更新数量
        const newQuantity = existingItem.quantity + quantity;
        if (newQuantity > availableStock) {
          throw new Error('库存不足');
        }

        return existingItem.update({
          quantity: newQuantity,
          total: price * newQuantity
        }, { transaction: t });
      } else {
        // 创建新的购物车项目
        return this.Model.create({
          userId: user.id,
          productId,
          variantId,
          quantity,
          price,
          total: price * quantity
        }, { transaction: t });
      }
    });
  }

  // 更新购物车项目
  async updateCartItem(id, data, params) {
    const { user } = params;
    const { quantity } = data;

    return this.Model.sequelize.transaction(async (t) => {
      const cartItem = await this.Model.findOne({
        where: { id, userId: user.id },
        include: ['product', 'variant'],
        transaction: t
      });

      if (!cartItem) {
        throw new Error('购物车项目不存在');
      }

      // 检查库存
      const availableStock = cartItem.variant?.stock || cartItem.product.stock;
      if (quantity > availableStock) {
        throw new Error('库存不足');
      }

      const price = cartItem.variant?.price || cartItem.product.price;
      
      return cartItem.update({
        quantity,
        total: price * quantity
      }, { transaction: t });
    });
  }

  // 获取用户购物车
  async getUserCart(params) {
    const { user } = params;

    const cartItems = await this.Model.findAll({
      where: { userId: user.id },
      include: [
        {
          model: this.app.service('products').Model,
          as: 'product',
          attributes: ['id', 'name', 'images', 'status', 'stock']
        },
        {
          model: this.app.service('productVariants').Model,
          as: 'variant',
          attributes: ['id', 'name', 'attributes', 'images', 'stock', 'price'],
          required: false
        }
      ],
      order: [['createdAt', 'DESC']]
    });

    // 计算总计
    const subtotal = cartItems.reduce((sum, item) => sum + parseFloat(item.total), 0);
    const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);

    return {
      items: cartItems,
      summary: {
        totalItems,
        subtotal,
        estimatedTotal: subtotal  // 这里可以加上运费等
      }
    };
  }

  // 清空购物车
  async clearCart(params) {
    const { user } = params;

    await this.Model.destroy({
      where: { userId: user.id }
    });

    return { message: '购物车已清空' };
  }

  // 批量删除购物车项目
  async removeItems(data, params) {
    const { user } = params;
    const { itemIds } = data;

    await this.Model.destroy({
      where: {
        id: { [Op.in]: itemIds },
        userId: user.id
      }
    });

    return { message: '已删除选中商品' };
  }

  // 检查购物车有效性
  async validateCart(params) {
    const { user } = params;

    const cartItems = await this.Model.findAll({
      where: { userId: user.id },
      include: ['product', 'variant']
    });

    const invalidItems = [];
    const validItems = [];

    for (const item of cartItems) {
      const product = item.product;
      const variant = item.variant;

      // 检查商品状态
      if (product.status !== 'active') {
        invalidItems.push({
          item,
          reason: '商品已下架'
        });
        continue;
      }

      // 检查变体状态
      if (variant && !variant.isActive) {
        invalidItems.push({
          item,
          reason: '商品规格已下架'
        });
        continue;
      }

      // 检查库存
      const availableStock = variant?.stock || product.stock;
      if (item.quantity > availableStock) {
        invalidItems.push({
          item,
          reason: `库存不足,当前库存:\${availableStock}`
        });
        continue;
      }

      // 检查价格变化
      const currentPrice = variant?.price || product.price;
      if (parseFloat(item.price) !== parseFloat(currentPrice)) {
        // 更新价格
        await item.update({
          price: currentPrice,
          total: currentPrice * item.quantity
        });
      }

      validItems.push(item);
    }

    return {
      validItems,
      invalidItems,
      isValid: invalidItems.length === 0
    };
  }
}

module.exports = CartService;

2. 订单服务

javascript
// src/services/orders/orders.class.js
const { Service } = require('feathers-sequelize');

class OrdersService extends Service {
  constructor(options, app) {
    super(options, app);
    this.app = app;
  }

  // 创建订单
  async create(data, params) {
    const { user } = params;
    const { 
      shippingAddressId, 
      paymentMethod, 
      couponCode,
      pointsToUse = 0,
      notes,
      invoiceInfo 
    } = data;

    return this.Model.sequelize.transaction(async (t) => {
      // 1. 验证购物车
      const cartValidation = await this.app.service('cart').validateCart({ user });
      if (!cartValidation.isValid) {
        throw new Error('购物车中有无效商品,请重新选择');
      }

      const cartItems = cartValidation.validItems;
      if (cartItems.length === 0) {
        throw new Error('购物车为空');
      }

      // 2. 获取收货地址
      const shippingAddress = await this.app.service('userAddresses').get(shippingAddressId, {
        query: { userId: user.id },
        transaction: t
      });

      // 3. 计算金额
      const subtotal = cartItems.reduce((sum, item) => sum + parseFloat(item.total), 0);
      let discountAmount = 0;
      let couponDiscount = 0;
      let couponId = null;

      // 处理优惠券
      if (couponCode) {
        const couponResult = await this.applyCoupon(couponCode, subtotal, user.id, t);
        couponId = couponResult.coupon.id;
        couponDiscount = couponResult.discount;
        discountAmount += couponDiscount;
      }

      // 处理积分
      let pointsUsed = 0;
      if (pointsToUse > 0) {
        const pointsResult = await this.usePoints(pointsToUse, user.id, subtotal - discountAmount, t);
        pointsUsed = pointsResult.pointsUsed;
        discountAmount += pointsResult.discount;
      }

      // 计算运费
      const shippingFee = await this.calculateShippingFee(cartItems, shippingAddress);
      
      // 计算税费
      const taxAmount = await this.calculateTax(subtotal - discountAmount, shippingAddress);
      
      const totalAmount = subtotal - discountAmount + shippingFee + taxAmount;

      // 4. 创建订单
      const orderNumber = this.Model.generateOrderNumber();
      
      const order = await this.Model.create({
        orderNumber,
        userId: user.id,
        shippingAddress: shippingAddress.toJSON(),
        subtotal,
        shippingFee,
        taxAmount,
        discountAmount,
        totalAmount,
        couponId,
        couponDiscount,
        pointsUsed,
        pointsEarned: Math.floor(totalAmount * 0.01), // 1% 积分返还
        notes,
        invoiceRequired: !!invoiceInfo,
        invoiceInfo
      }, { transaction: t });

      // 5. 创建订单项目
      const orderItems = [];
      for (const cartItem of cartItems) {
        const orderItem = await this.app.service('orderItems').create({
          orderId: order.id,
          productId: cartItem.productId,
          variantId: cartItem.variantId,
          productName: cartItem.product.name,
          productImage: cartItem.product.images[0] || null,
          variantAttributes: cartItem.variant?.attributes || null,
          price: cartItem.price,
          quantity: cartItem.quantity,
          total: cartItem.total
        }, { transaction: t });

        orderItems.push(orderItem);

        // 减少库存
        if (cartItem.variantId) {
          await this.app.service('productVariants').Model.decrement('stock', {
            by: cartItem.quantity,
            where: { id: cartItem.variantId },
            transaction: t
          });
        } else {
          await this.app.service('products').Model.decrement('stock', {
            by: cartItem.quantity,
            where: { id: cartItem.productId },
            transaction: t
          });
        }
      }

      // 6. 清空购物车
      await this.app.service('cart').Model.destroy({
        where: { userId: user.id },
        transaction: t
      });

      // 7. 扣除积分
      if (pointsUsed > 0) {
        await this.app.service('users').Model.decrement('points', {
          by: pointsUsed,
          where: { id: user.id },
          transaction: t
        });
      }

      // 8. 标记优惠券为已使用
      if (couponId) {
        await this.app.service('userCoupons').Model.update(
          { usedAt: new Date() },
          {
            where: {
              userId: user.id,
              couponId: couponId,
              usedAt: null
            },
            transaction: t
          }
        );
      }

      return {
        order,
        items: orderItems
      };
    });
  }

  // 应用优惠券
  async applyCoupon(couponCode, orderAmount, userId, transaction) {
    const coupon = await this.app.service('coupons').Model.findOne({
      where: {
        code: couponCode,
        isActive: true,
        startDate: { [Op.lte]: new Date() },
        endDate: { [Op.gte]: new Date() }
      },
      transaction
    });

    if (!coupon) {
      throw new Error('优惠券不存在或已过期');
    }

    // 检查使用条件
    if (coupon.minAmount && orderAmount < coupon.minAmount) {
      throw new Error(`订单金额需满 \${coupon.minAmount} 元才能使用此优惠券`);
    }

    // 检查用户是否已使用
    const userCoupon = await this.app.service('userCoupons').Model.findOne({
      where: {
        userId,
        couponId: coupon.id,
        usedAt: null
      },
      transaction
    });

    if (!userCoupon) {
      throw new Error('您没有此优惠券或已使用过');
    }

    // 计算折扣
    let discount = 0;
    if (coupon.type === 'fixed') {
      discount = Math.min(coupon.value, orderAmount);
    } else if (coupon.type === 'percentage') {
      discount = orderAmount * (coupon.value / 100);
      if (coupon.maxDiscount) {
        discount = Math.min(discount, coupon.maxDiscount);
      }
    }

    return {
      coupon,
      discount: Math.round(discount * 100) / 100
    };
  }

  // 使用积分
  async usePoints(pointsToUse, userId, orderAmount, transaction) {
    const user = await this.app.service('users').Model.findByPk(userId, {
      transaction
    });

    if (user.points < pointsToUse) {
      throw new Error('积分不足');
    }

    // 积分兑换比例:100积分 = 1元
    const discount = Math.min(pointsToUse / 100, orderAmount);
    const actualPointsUsed = Math.floor(discount * 100);

    return {
      pointsUsed: actualPointsUsed,
      discount: Math.round(discount * 100) / 100
    };
  }

  // 计算运费
  async calculateShippingFee(cartItems, shippingAddress) {
    // 简单的运费计算逻辑
    const totalWeight = cartItems.reduce((sum, item) => {
      const weight = item.variant?.weight || item.product.weight || 0;
      return sum + (weight * item.quantity);
    }, 0);

    if (totalWeight === 0) {
      return 0; // 虚拟商品免运费
    }

    // 基础运费
    let shippingFee = 10;

    // 按重量计算
    if (totalWeight > 1) {
      shippingFee += Math.ceil((totalWeight - 1) / 0.5) * 5;
    }

    // 偏远地区加收
    const remoteAreas = ['西藏', '新疆', '内蒙古'];
    if (remoteAreas.includes(shippingAddress.province)) {
      shippingFee += 20;
    }

    return shippingFee;
  }

  // 计算税费
  async calculateTax(amount, shippingAddress) {
    // 简单的税费计算,实际应用中需要根据地区和商品类型计算
    return 0;
  }

  // 取消订单
  async cancelOrder(id, data, params) {
    const { user } = params;
    const { reason } = data;

    return this.Model.sequelize.transaction(async (t) => {
      const order = await this.get(id, {
        query: { $populate: ['items'] },
        transaction: t
      });

      // 检查权限
      if (order.userId !== user.id && user.role !== 'admin') {
        throw new Error('无权限取消此订单');
      }

      // 检查订单状态
      if (!order.canCancel()) {
        throw new Error('订单当前状态不允许取消');
      }

      // 恢复库存
      for (const item of order.items) {
        if (item.variantId) {
          await this.app.service('productVariants').Model.increment('stock', {
            by: item.quantity,
            where: { id: item.variantId },
            transaction: t
          });
        } else {
          await this.app.service('products').Model.increment('stock', {
            by: item.quantity,
            where: { id: item.productId },
            transaction: t
          });
        }
      }

      // 退还积分
      if (order.pointsUsed > 0) {
        await this.app.service('users').Model.increment('points', {
          by: order.pointsUsed,
          where: { id: order.userId },
          transaction: t
        });
      }

      // 退还优惠券
      if (order.couponId) {
        await this.app.service('userCoupons').Model.update(
          { usedAt: null },
          {
            where: {
              userId: order.userId,
              couponId: order.couponId
            },
            transaction: t
          }
        );
      }

      // 更新订单状态
      return order.update({
        status: 'cancelled',
        notes: reason ? `取消原因:\${reason}` : order.notes
      }, { transaction: t });
    });
  }

  // 确认收货
  async confirmDelivery(id, params) {
    const { user } = params;

    const order = await this.get(id);

    if (order.userId !== user.id) {
      throw new Error('无权限操作此订单');
    }

    if (order.status !== 'delivered') {
      throw new Error('订单未发货或已确认收货');
    }

    return this.Model.sequelize.transaction(async (t) => {
      // 更新订单状态
      await order.update({
        status: 'completed'
      }, { transaction: t });

      // 增加用户积分
      if (order.pointsEarned > 0) {
        await this.app.service('users').Model.increment('points', {
          by: order.pointsEarned,
          where: { id: order.userId },
          transaction: t
        });
      }

      // 更新用户消费总额
      await this.app.service('users').Model.increment('totalSpent', {
        by: order.totalAmount,
        where: { id: order.userId },
        transaction: t
      });

      return order;
    });
  }
}

module.exports = OrdersService;

总结

通过这个电商系统项目,我们综合应用了 Feathers.js + Sequelize 的各种特性:

完整的数据模型设计

  • 用户和地址管理
  • 商品和变体系统
  • 订单和支付流程

复杂的业务逻辑

  • 购物车管理
  • 库存控制
  • 优惠券和积分系统

事务处理应用

  • 订单创建的原子性
  • 库存扣减的一致性
  • 支付状态的可靠性

性能优化实践

  • 查询优化
  • 索引设计
  • 缓存策略

这个项目展示了如何用 Feathers.js + Sequelize 构建复杂的企业级应用,是学习和实践的绝佳案例。


Feathers.js + Sequelize 系列文章:

感谢大家的阅读!如有问题欢迎留言讨论!