evershop预售模式:商品预售与众筹功能实现
【免费下载链接】evershop 🛍️ NodeJS E-commerce Platform 项目地址: https://gitcode.com/GitHub_Trending/ev/evershop
前言:电商预售的痛点与机遇
在当今竞争激烈的电商环境中,传统现货销售模式已无法满足所有业务需求。你是否遇到过这些场景:
- 新品上市前想要测试市场反应,但不敢大量备货?
- 限量版商品想要确保每个真正想要的用户都能买到?
- 创意产品需要筹集启动资金才能投入生产?
- 季节性商品想要提前锁定订单,优化供应链?
evershop作为一款现代化的NodeJS电商平台,虽然原生未提供完整的预售功能,但其灵活的架构为我们实现预售与众筹模式提供了完美的基础。本文将深入探讨如何在evershop中实现专业的预售系统。
技术架构设计
数据库扩展方案
首先我们需要扩展产品模型,添加预售相关字段:
-- 添加预售相关字段到product表
ALTER TABLE product ADD COLUMN pre_sale_enabled BOOLEAN DEFAULT false;
ALTER TABLE product ADD COLUMN pre_sale_start_date TIMESTAMP;
ALTER TABLE product ADD COLUMN pre_sale_end_date TIMESTAMP;
ALTER TABLE product ADD COLUMN pre_sale_target_qty INTEGER DEFAULT 0;
ALTER TABLE product ADD COLUMN pre_sale_actual_qty INTEGER DEFAULT 0;
ALTER TABLE product ADD COLUMN pre_sale_price DECIMAL(10,2);
ALTER TABLE product ADD COLUMN pre_sale_type VARCHAR(20) DEFAULT 'presale'; -- presale/crowdfunding
预售状态机设计
核心功能实现
1. 产品服务层扩展
在createProduct.ts基础上扩展预售功能:
// packages/evershop/src/modules/catalog/services/product/extensions/preSaleProduct.ts
export type PreSaleProductData = ProductData & {
pre_sale_enabled: boolean;
pre_sale_start_date?: string;
pre_sale_end_date?: string;
pre_sale_target_qty?: number;
pre_sale_price?: number;
pre_sale_type?: 'presale' | 'crowdfunding';
};
async function validatePreSaleData(data: PreSaleProductData) {
if (data.pre_sale_enabled) {
if (!data.pre_sale_start_date || !data.pre_sale_end_date) {
throw new Error('预售商品必须设置开始和结束时间');
}
if (data.pre_sale_start_date >= data.pre_sale_end_date) {
throw new Error('预售开始时间必须早于结束时间');
}
if (data.pre_sale_type === 'crowdfunding' && !data.pre_sale_target_qty) {
throw new Error('众筹商品必须设置目标数量');
}
}
}
// 扩展createProduct函数
export async function createPreSaleProduct(data: PreSaleProductData, context: Record<string, any>) {
await validatePreSaleData(data);
// 原有创建逻辑...
const product = await createProduct(data, context);
if (data.pre_sale_enabled) {
// 设置库存为0,仅预售可用
await update('product_inventory')
.given({ qty: 0, stock_availability: false })
.where('product_inventory_product_id', '=', product.insertId)
.execute(connection);
}
return product;
}
2. 购物车逻辑改造
// packages/evershop/src/modules/checkout/services/cartItemCreator.ts
export async function addPreSaleItemToCart(cartId: number, productId: number, qty: number) {
const product = await select()
.from('product')
.where('product_id', '=', productId)
.load(connection);
if (product.pre_sale_enabled) {
const now = new Date();
const startDate = new Date(product.pre_sale_start_date);
const endDate = new Date(product.pre_sale_end_date);
if (now < startDate) {
throw new Error('预售尚未开始');
}
if (now > endDate) {
throw new Error('预售已结束');
}
// 检查众筹目标是否达成
if (product.pre_sale_type === 'crowdfunding') {
const currentQty = await getPreSaleCurrentQty(productId);
if (currentQty + qty > product.pre_sale_target_qty) {
throw new Error('超过众筹目标数量');
}
}
}
// 原有添加购物车逻辑...
}
3. 订单处理流程
// packages/evershop/src/modules/checkout/services/orderCreator.ts
export async function createPreSaleOrder(orderData: any) {
const preSaleItems = orderData.items.filter(item => item.pre_sale_enabled);
if (preSaleItems.length > 0) {
// 设置订单状态为预售中
orderData.payment_status = 'pre_sale_pending';
orderData.shipment_status = 'pre_sale_awaiting';
// 创建预售订单
const order = await createOrder(orderData);
// 更新预售数量统计
for (const item of preSaleItems) {
await update('product')
.set('pre_sale_actual_qty', 'pre_sale_actual_qty + ?', [item.qty])
.where('product_id', '=', item.product_id)
.execute(connection);
}
return order;
}
return await createOrder(orderData);
}
前端界面实现
产品详情页预售组件
// packages/evershop/src/components/frontStore/PreSaleInfo.jsx
import React from 'react';
import { useTranslation } from '../../lib/locale/translate';
const PreSaleInfo = ({ product }) => {
const { t } = useTranslation();
const now = new Date();
const startDate = new Date(product.pre_sale_start_date);
const endDate = new Date(product.pre_sale_end_date);
const getProgressPercentage = () => {
if (product.pre_sale_type === 'crowdfunding') {
return Math.min((product.pre_sale_actual_qty / product.pre_sale_target_qty) * 100, 100);
}
return 0;
};
const getStatusText = () => {
if (now < startDate) {
return t('pre_sale.coming_soon', { date: startDate.toLocaleDateString() });
} else if (now > endDate) {
return product.pre_sale_actual_qty >= product.pre_sale_target_qty
? t('pre_sale.successful')
: t('pre_sale.failed');
} else {
return t('pre_sale.in_progress');
}
};
return (
<div className="pre-sale-info">
<h3>{product.pre_sale_type === 'crowdfunding' ? t('crowdfunding') : t('pre_sale')}</h3>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${getProgressPercentage()}%` }}
/>
</div>
<div className="pre-sale-stats">
<span>{product.pre_sale_actual_qty} / {product.pre_sale_target_qty} {t('units')}</span>
<span>{Math.round(getProgressPercentage())}%</span>
</div>
<div className="pre-sale-status">{getStatusText()}</div>
<div className="pre-sale-timer">
{now < endDate && (
<Countdown endDate={endDate} />
)}
</div>
</div>
);
};
后台管理界面
预售商品管理表格
// packages/evershop/src/components/admin/PreSaleGrid.jsx
import React from 'react';
import { DataGrid } from '../common/DataGrid';
const PreSaleGrid = () => {
const columns = [
{
field: 'name',
headerName: '商品名称',
width: 200
},
{
field: 'pre_sale_type',
headerName: '预售类型',
width: 120,
renderCell: (params) => (
<span className={`badge ${params.value}`}>
{params.value === 'presale' ? '预售' : '众筹'}
</span>
)
},
{
field: 'pre_sale_actual_qty',
headerName: '已售数量',
width: 100
},
{
field: 'pre_sale_target_qty',
headerName: '目标数量',
width: 100
},
{
field: 'progress',
headerName: '完成进度',
width: 150,
renderCell: (params) => {
const progress = (params.row.pre_sale_actual_qty / params.row.pre_sale_target_qty) * 100;
return (
<div className="progress-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${Math.min(progress, 100)}%` }}
/>
</div>
<span>{Math.round(progress)}%</span>
</div>
);
}
},
{
field: 'status',
headerName: '状态',
width: 100,
renderCell: (params) => {
const now = new Date();
const endDate = new Date(params.row.pre_sale_end_date);
const progress = (params.row.pre_sale_actual_qty / params.row.pre_sale_target_qty) * 100;
let status = '进行中';
if (now > endDate) {
status = progress >= 100 ? '成功' : '失败';
} else if (now < new Date(params.row.pre_sale_start_date)) {
status = '未开始';
}
return <span className={`status ${status}`}>{status}</span>;
}
}
];
return (
<DataGrid
columns={columns}
queryKey="preSaleProducts"
baseQuery={getPreSaleProductsBaseQuery}
/>
);
};
支付与退款流程
预售支付处理
// packages/evershop/src/modules/oms/services/preSalePaymentHandler.ts
export class PreSalePaymentHandler {
async processPayment(order: any, paymentData: any) {
if (order.payment_status === 'pre_sale_pending') {
// 预售订单支付处理
const paymentResult = await this.processPreSalePayment(order, paymentData);
if (paymentResult.success) {
// 更新订单状态
await update('order')
.given({ payment_status: 'pre_sale_paid' })
.where('order_id', '=', order.order_id)
.execute(connection);
// 检查众筹目标是否达成
await this.checkFundingGoal(order);
}
return paymentResult;
}
// 正常订单处理逻辑
return await processNormalPayment(order, paymentData);
}
async processRefund(order: any, refundData: any) {
if (order.payment_status === 'pre_sale_paid') {
// 预售订单退款特殊处理
const refundResult = await this.processPreSaleRefund(order, refundData);
if (refundResult.success) {
// 更新预售数量统计
for (const item of order.items) {
if (item.pre_sale_enabled) {
await update('product')
.set('pre_sale_actual_qty', 'pre_sale_actual_qty - ?', [item.qty])
.where('product_id', '=', item.product_id)
.execute(connection);
}
}
}
return refundResult;
}
// 正常退款逻辑
return await processNormalRefund(order, refundData);
}
}
自动化任务与通知
预售状态监控任务
// packages/evershop/src/lib/cronjob/preSaleMonitor.ts
export class PreSaleMonitorJob {
async execute() {
// 检查即将开始的预售
const upcomingPreSales = await select()
.from('product')
.where('pre_sale_enabled', '=', true)
.and('pre_sale_start_date', '>', new Date())
.and('pre_sale_start_date', '<', new Date(Date.now() + 24 * 60 * 60 * 1000)) // 24小时内开始
.execute(connection);
for (const product of upcomingPreSales) {
await this.sendUpcomingNotification(product);
}
// 检查已结束的预售
const endedPreSales = await select()
.from('product')
.where('pre_sale_enabled', '=', true)
.and('pre_sale_end_date', '<', new Date())
.and('pre_sale_processed', '=', false)
.execute(connection);
for (const product of endedPreSales) {
await this.processEndedPreSale(product);
}
}
private async processEndedPreSale(product: any) {
const success = product.pre_sale_actual_qty >= product.pre_sale_target_qty;
if (success) {
// 众筹成功,开始生产/发货流程
await this.handleSuccessfulFunding(product);
} else {
// 众筹失败,处理退款
await this.handleFailedFunding(product);
}
// 标记为已处理
await update('product')
.given({ pre_sale_processed: true })
.where('product_id', '=', product.product_id)
.execute(connection);
}
}
测试策略与质量保证
单元测试示例
// tests/unit/preSale/preSaleService.test.ts
describe('PreSaleService', () => {
describe('createPreSaleProduct', () => {
it('应该成功创建预售商品', async () => {
const productData = {
name: '测试预售商品',
sku: 'TEST-PRE-001',
price: 100,
pre_sale_enabled: true,
pre_sale_start_date: new Date(Date.now() + 86400000).toISOString(), // 明天
pre_sale_end_date: new Date(Date.now() + 86400000 * 7).toISOString(), // 7天后
pre_sale_target_qty: 100,
pre_sale_type: 'crowdfunding'
};
const product = await createPreSaleProduct(productData);
expect(product.pre_sale_enabled).toBe(true);
expect(product.pre_sale_actual_qty).toBe(0);
});
it('应该拒绝无效的预售时间设置', async () => {
const productData = {
name: '测试商品',
sku: 'TEST-001',
price: 100,
pre_sale_enabled: true,
pre_sale_start_date: new Date(Date.now() + 86400000).toISOString(),
pre_sale_end_date: new Date(Date.now() - 86400000).toISOString(), // 结束时间早于开始时间
pre_sale_target_qty: 100
};
await expect(createPreSaleProduct(productData)).rejects.toThrow();
});
});
});
部署与运维考虑
环境配置
// config/preSale.js
module.exports = {
// 预售相关配置
preSale: {
// 自动处理间隔(分钟)
processInterval: 30,
// 邮件通知配置
notifications: {
upcoming: {
enabled: true,
leadTime: 24 // 小时
},
success: {
enabled: true
},
failure: {
enabled: true
}
},
// 退款策略
refundPolicy: {
automatic: true,
processingTime: 7 // 天数
}
}
};
总结与最佳实践
通过本文的实施方案,我们为evershop构建了完整的预售与众筹功能体系。关键收获:
- 架构灵活性:evershop的模块化设计使得功能扩展变得简单
- 数据完整性:通过合理的数据库设计和状态管理确保业务逻辑的严谨性
- 用户体验:前端组件的精心设计提供了清晰的预售信息展示
- 自动化运维:定时任务和通知系统减少了人工干预需求
实施建议表格
| 功能模块 | 实施优先级 | 预计工时 | 技术复杂度 |
|---|---|---|---|
| 数据库扩展 | 高 | 2小时 | 低 |
| 产品服务层 | 高 | 4小时 | 中 |
| 购物车逻辑 | 高 | 3小时 | 中 |
| 订单处理 | 高 | 4小时 | 高 |
| 前端组件 | 中 | 6小时 | 中 |
| 后台管理 | 中 | 4小时 | 中 |
| 支付集成 | 高 | 8小时 | 高 |
| 自动化任务 | 低 | 3小时 | 中 |
成功关键指标
通过这套预售系统,商家可以更好地管理产品生命周期,降低库存风险,同时为用户提供更多样的购物体验。evershop的扩展性再次证明了其作为现代电商平台的强大潜力。
【免费下载链接】evershop 🛍️ NodeJS E-commerce Platform 项目地址: https://gitcode.com/GitHub_Trending/ev/evershop
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



