跳到主要内容

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

· 阅读需 16 分钟
一介布衣
全栈开发者

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

前言

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

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

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

项目架构设计

核心功能模块

用户系统

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

商品系统

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

订单系统

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

支付系统

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

营销系统

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

数据模型设计

1. 用户相关模型

// 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. 商品相关模型

// 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. 订单相关模型

// 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. 购物车服务

// 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. 订单服务

// 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 系列文章:

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