evershop预售模式:商品预售与众筹功能实现

evershop预售模式:商品预售与众筹功能实现

【免费下载链接】evershop 🛍️ NodeJS E-commerce Platform 【免费下载链接】evershop 项目地址: 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

预售状态机设计

mermaid

核心功能实现

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构建了完整的预售与众筹功能体系。关键收获:

  1. 架构灵活性:evershop的模块化设计使得功能扩展变得简单
  2. 数据完整性:通过合理的数据库设计和状态管理确保业务逻辑的严谨性
  3. 用户体验:前端组件的精心设计提供了清晰的预售信息展示
  4. 自动化运维:定时任务和通知系统减少了人工干预需求

实施建议表格

功能模块实施优先级预计工时技术复杂度
数据库扩展2小时
产品服务层4小时
购物车逻辑3小时
订单处理4小时
前端组件6小时
后台管理4小时
支付集成8小时
自动化任务3小时

成功关键指标

mermaid

通过这套预售系统,商家可以更好地管理产品生命周期,降低库存风险,同时为用户提供更多样的购物体验。evershop的扩展性再次证明了其作为现代电商平台的强大潜力。

【免费下载链接】evershop 🛍️ NodeJS E-commerce Platform 【免费下载链接】evershop 项目地址: https://gitcode.com/GitHub_Trending/ev/evershop

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值