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 系列文章:
- Feathers.js + Sequelize 基础集成 - 关系型数据库的完美搭档
- Feathers.js + Sequelize 模型设计与关联 - 构建复杂的数据关系
- Feathers.js + Sequelize 高级查询与优化 - 构建高性能应用
感谢大家的阅读!如有问题欢迎留言讨论!