1.1 原生营销活动架构
LikeShop 的营销模块采用插件化、模块化的开发思路,所有营销玩法都做到业务轻耦合、代码轻耦合;营销功能统一放在 app/common/model 和 app/shopapi/logic/ 下,营销模块不直接操作订单,而是通过规则校验和价格计算的方式接入主交易链路。
一次下单请求中,营销逻辑的介入点大致为:用户提交订单 → OrderLogic::create() → 优惠券校验与抵扣计算 → 积分抵扣计算 → 分销关系绑定与佣金预计算 → 生成订单并写入营销快照数据;这个扩展点设计为预售活动的接入提供了天然的基础。
原生系统的活动管理采用 ls_activity_area(活动区域表)和 ls_activity_goods(活动区域商品表)的两层架构,活动以区域维度组织商品,但活动类型组织商品,但活动类型相对基础,主要面向常规促销场景。
1.2 预售场景的四个核心缺口
缺口一:无定金与尾款的两段式支付模型。原生的支付链路是一次性完成支付(REATED → PAYING → PAID),不支持“先付定金锁定资格、后付尾款完成交易”的两段模式;定金订单和尾款订单在原生系统中没有对应的数据结构,订单状态机也无法表达“已付定金、待付尾款”这一中间态。
缺口二:库存扣减时机不明确。原生库存扣减发生在订单支付成功后,但预售场景下有两种合理的扣减策略:定金阶段预扣(锁定库存防止超卖)和尾款阶段实扣;原生系统缺乏配置化和减时机控制,也无法在定金阶段对预售库存做独立管控。
缺口三:无尾款支付时间窗口。原生系统不感知“尾款支付开始时间”和“尾款支付结束时间”两个关键节点;预售活动中,用户支付定金后需要等待尾款支付窗口开启才能付款,窗口关闭后订单自动失效;这套时间驱动的自动化逻辑在原生活动体系中完全缺失。
缺口四:无定金膨胀与定金立减的差异化定价。原生活动的价格计算只处理“原价 - 优惠券 - 积分”的线性模型;预售场景需要支持定金膨胀(定金在尾款支付时按倍率抵扣)和定金扣减(从尾款中直接扣除固定金额)两种差异化定价策略,且需要与阶梯优惠、优惠券、积分形成多层叠加;原生价格引擎缺乏这种多层联动的计算能力。
1.3 二开策略定位
遵循 LikeShop 的二开规范,预售模块采用以下策略:核心业务链路保持稳定,所有扩展基于外围能力实现,新建独立的 PresaleService 和预售数据表体系,不修改 OrderLogic、PayLogic 的核心逻辑;预售订单的状态机扩展通过增加新的订单类型和状态枚举实现,而非修改已有的状态流转逻辑。
2.1 预售活动配置表
预售活动配置表是预售模块的核心,承载定位规则、尾款时间窗口、库存策略等全部配置:
sql
CREATE TABLE `ls_presale_activity` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`activity_name` varchar(100) NOT NULL COMMENT '活动名称',
`goods_id` int UNSIGNED NOT NULL COMMENT '商品ID',
`item_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '规格ID,0为多规格商品',
`presale_price` decimal(10,2) NOT NULL COMMENT '预售价(原价)',
`deposit_amount` decimal(10,2) NOT NULL COMMENT '定金金额',
`deposit_expand_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '定金膨胀金额(尾款可抵扣金额,必须≥定金金额)',
`deposit_type` tinyint NOT NULL DEFAULT 1 COMMENT '定金类型:1-定金膨胀,2-定金立减',
`deposit_reduce_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '定金立减金额(直接从尾款中扣除)',
`presale_stock` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '预售专属库存',
`stock_deduct_type` tinyint NOT NULL DEFAULT 1 COMMENT '库存扣减时机:1-付定金时预扣,2-付尾款时实扣',
`deposit_start_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '定金支付开始时间',
`deposit_end_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '定金支付结束时间',
`balance_start_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '尾款支付开始时间',
`balance_end_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '尾款支付结束时间',
`delivery_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '预计发货时间',
`is_ladder` tinyint NOT NULL DEFAULT 0 COMMENT '是否开启阶梯优惠:0-否,1-是',
`ladder_config` text COMMENT '阶梯优惠配置,JSON格式:[{"num":50,"discount":0.8},{"num":100,"discount":0.7}]',
`limit_buy` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '每人限购数量,0为不限',
`deposit_refundable` tinyint NOT NULL DEFAULT 0 COMMENT '定金是否可退:0-不可退(需付尾款后申请退货),1-可退',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
`update_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_goods_id` (`goods_id`),
KEY `idx_status_time` (`status`, `deposit_start_time`, `deposit_end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预售活动配置表';
deposit_type 字段区分两种定金策略:定金膨胀(deposit_expand_amount)和定金立减(deposit_reduce_amount);定金膨胀优惠金额是指定金在支付尾款时可以优惠的金额,必须大于等于定金本身;例如设置定金100、定金膨胀金额200,则尾款支付时自动优惠200;定金立减则是从尾款中直接扣除固定金额,计算更简单直接。
ladder_config 字段支持阶梯优惠,即达到目标人数后进行折扣的功能;例如设置满50件8折、满100件7折,当预售定金时间结束时根据实际下单件数匹配对应的折扣档位。
2.2 预售订单扩展表
在订单表之外新建预售订单扩展表,记录预售专属的定金、尾款、库存等数据:
sql
CREATE TABLE `ls_presale_order` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`activity_id` int UNSIGNED NOT NULL COMMENT '预售活动ID',
`user_id` int UNSIGNED NOT NULL COMMENT '用户ID',
`deposit_order_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '定金订单ID(ls_order.id)',
`deposit_order_sn` varchar(32) NOT NULL DEFAULT '' COMMENT '定金订单号',
`balance_order_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '尾款订单ID',
`balance_order_sn` varchar(32) NOT NULL DEFAULT '' COMMENT '尾款订单号',
`goods_id` int UNSIGNED NOT NULL COMMENT '商品ID',
`item_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '规格ID',
`goods_num` int UNSIGNED NOT NULL DEFAULT 1 COMMENT '购买数量',
`deposit_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '定金金额快照',
`deposit_expand_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '定金膨胀/立减金额快照',
`presale_price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '预售价快照',
`balance_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '尾款应付金额',
`balance_paid_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '尾款实付金额',
`deposit_pay_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '定金支付时间',
`balance_pay_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '尾款支付时间',
`balance_expire_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '尾款支付截止时间',
`delivery_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '预计发货时间',
`stock_locked` tinyint NOT NULL DEFAULT 0 COMMENT '库存是否已锁定:0-未锁定,1-已锁定',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-待付定金,2-已付定金待付尾款,3-已付尾款,4-尾款超时关闭,5-已退款',
`remind_status` tinyint NOT NULL DEFAULT 0 COMMENT '尾款提醒状态:0-未提醒,1-已提醒',
`create_time` int UNSIGNED NOT NULL DEFAULT 0,
`update_time` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_activity_id` (`activity_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status_expire` (`status`, `balance_expire_time`),
UNIQUE KEY `uk_deposit_order` (`deposit_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预售订单扩展表';
status 字段的枚举设计涵盖了预售订单的完整生命周期:1-待付定金(用户已下单但未付定金)、2-已付定金待付尾款(核心中间态)、3-已付尾款(尾款支付完成)、4-尾款超时关闭(尾款支付窗口关闭后未支付)、5-已退款;balance_expire_time 字段是尾款自动关闭定时任务的关键依据。
3.1 定金阶段的核心逻辑
用户点击支付定金时,系统需要完成以下校验和操作:校验用户是否在限购范围内、校验预售库存是否充足、锁定库存(如果配置为定金阶段预扣)、创建定金订单并记录快照。
php
namespace app\common\service;
class PresaleDepositService
{
/**
* 用户支付定金
*/
public function payDeposit($activityId, $userId, $goodsNum = 1)
{
$activity = PresaleActivityModel::where('id', $activityId)
->where('status', 1)->find();
if (!$activity) {
throw new \Exception('预售活动不存在或已结束');
}
// 1. 校验定金支付时间窗口
$now = time();
if ($now < $activity['deposit_start_time']) {
throw new \Exception('定金支付尚未开始');
}
if ($now > $activity['deposit_end_time']) {
throw new \Exception('定金支付已结束');
}
// 2. 校验限购
if ($activity['limit_buy'] > 0) {
$boughtNum = PresaleOrderModel::where('activity_id', $activityId)
->where('user_id', $userId)
->whereIn('status', [2, 3])
->sum('goods_num');
if ($boughtNum + $goodsNum > $activity['limit_buy']) {
throw new \Exception("该预售商品每人限购{$activity['limit_buy']}件");
}
}
// 3. 校验预售库存并锁定
$this->checkAndLockStock($activity, $goodsNum);
// 4. 创建预售订单记录
Db::startTrans();
try {
$presaleOrderId = PresaleOrderModel::create([
'activity_id' => $activityId,
'user_id' => $userId,
'goods_id' => $activity['goods_id'],
'item_id' => $activity['item_id'],
'goods_num' => $goodsNum,
'deposit_amount' => $activity['deposit_amount'],
'deposit_expand_amount' => $activity['deposit_type'] == 1
? $activity['deposit_expand_amount']
: $activity['deposit_reduce_amount'],
'presale_price' => $activity['presale_price'],
'balance_expire_time' => $activity['balance_end_time'],
'delivery_time' => $activity['delivery_time'],
'status' => 1,
'create_time' => $now,
])->id;
// 5. 创建定金订单(走原生订单体系,order_type 标识为预售定金)
$depositOrderSn = generateSn('PD');
$depositOrderId = OrderModel::create([
'order_sn' => $depositOrderSn,
'user_id' => $userId,
'order_type' => 6, // 6-预售定金订单
'order_amount' => $activity['deposit_amount'],
'goods_amount' => $activity['deposit_amount'],
'order_status' => 0, // 待付款
'pay_status' => 0,
'create_time' => $now,
])->id;
PresaleOrderModel::where('id', $presaleOrderId)->update([
'deposit_order_id' => $depositOrderId,
'deposit_order_sn' => $depositOrderSn,
]);
Db::commit();
return ['presale_order_id' => $presaleOrderId, 'order_sn' => $depositOrderSn];
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
/**
* 校验并锁定预售专属库存
*/
private function checkAndLockStock($activity, $goodsNum)
{
// 使用行锁防止并发超卖
$activity = PresaleActivityModel::lock(true)->find($activity['id']);
$lockedStock = PresaleOrderModel::where('activity_id', $activity['id'])
->whereIn('status', [1, 2])
->sum('goods_num');
$remainStock = $activity['presale_stock'] - $lockedStock;
if ($remainStock < $goodsNum) {
throw new \Exception('预售库存不足');
}
// 如果配置为定金阶段预扣,直接扣减预售专属库存
if ($activity['stock_deduct_type'] == 1) {
PresaleActivityModel::where('id', $activity['id'])
->dec('presale_stock', $goodsNum)->update();
}
}
}
预售库存独立管控的核心设计:在 ls_presale_activity 表中设置独立的 presale_stock 字段,与商品的常规库存(ls_goods_item.stock)物理隔离;预售活动通过独立的库存池运作,常规销售不受预售库占用影响,两者互不干扰;库存扣减时机通过 stock_deduct_type 配置化控制。
3.2 定金支付成功的处理
定金订单支付成功后,通过事件监听器更新预售订单状态:
php
class PresaleDepositPaidListener
{
public function handle($orderId)
{
$order = OrderModel::find($orderId);
if (!$order || $order['order_type'] != 6) return;
// 更新预售订单状态
PresaleOrderModel::where('deposit_order_id', $orderId)->update([
'status' => 2, // 已付定金待付尾款
'deposit_pay_time' => time(),
]);
// 发送定金支付成功通知(含尾款支付时间提醒)
$presaleOrder = PresaleOrderModel::where('deposit_order_id', $orderId)->find();
if ($presaleOrder) {
$activity = PresaleActivityModel::find($presaleOrder['activity_id']);
MessageService::send(
$presaleOrder['user_id'],
'预售定金支付成功',
"您已成功支付定金,尾款支付时间为 "
. date('Y-m-d H:i:s', $activity['balance_start_time'])
. " 至 " . date('Y-m-d H:i:s', $activity['balance_end_time'])
. ",请及时支付尾款。"
);
}
}
}
3.3 尾款金额计算引擎
尾款金额的计算是预售模块最核心的逻辑,需要按顺序叠加定金膨胀/立减、阶梯优惠、优惠券和积分;根据 LikeShop 原生的预售计算规则,下单计算规则为:
(售价/会员价x阶梯折扣-定金膨胀金-优惠券-积分抵扣)x 超级会员卡折扣
优惠券门槛计算是按“售价 x 阶梯折扣 - 定金膨胀金”来计算的,而非按会员价计算。
php
class PresaleBalanceService
{
/**
* 计算尾款金额
*/
public function calcBalanceAmount($presaleOrderId, $couponId = 0, $useIntegral = 0)
{
$presaleOrder = PresaleOrderModel::find($presaleOrderId);
if (!$presaleOrder || $presaleOrder['status'] != 2) {
throw new \Exception('预售订单状态不正确');
}
$activity = PresaleActivityModel::find($presaleOrder['activity_id']);
$presalePrice = $presaleOrder['presale_price'];
$goodsNum = $presaleOrder['goods_num'];
// 第一步:计算阶梯折扣后的价格
$ladderPrice = $presalePrice * $goodsNum;
if ($activity['is_ladder'] == 1 && !empty($activity['ladder_config'])) {
$ladderConfig = json_decode($activity['ladder_config'], true);
$totalNum = $this->getPresaleTotalNum($activity['id']);
$ladderPrice = $this->applyLadderDiscount($presalePrice, $goodsNum, $ladderConfig, $totalNum);
}
// 第二步:扣除定金膨胀/立减金额
$depositDeduct = $presaleOrder['deposit_expand_amount'];
// 第三步:计算优惠券抵扣(门槛按 "售价×阶梯折扣 - 定金膨胀金" 计算)
$couponAmount = 0;
if ($couponId > 0) {
$couponThreshold = $ladderPrice - $depositDeduct;
$couponAmount = $this->calcCouponAmount($couponId, $couponThreshold);
}
// 第四步:计算积分抵扣
// 积分最高抵扣价为:(售价/会员价 - 定金膨胀金额 - 优惠券)× 积分百分比
$integralAmount = 0;
if ($useIntegral > 0) {
$integralBase = $ladderPrice - $depositDeduct - $couponAmount;
$integralAmount = $this->calcIntegralDeduction(
$presaleOrder['user_id'], $useIntegral, $integralBase
);
}
// 第五步:最终尾款 = 阶梯价 - 定金膨胀 - 优惠券 - 积分
$balanceAmount = $ladderPrice - $depositDeduct - $couponAmount - $integralAmount;
$balanceAmount = max(0.01, round($balanceAmount, 2));
return [
'presale_price' => $presalePrice,
'ladder_price' => $ladderPrice,
'deposit_deduct' => $depositDeduct,
'coupon_amount' => $couponAmount,
'integral_amount' => $integralAmount,
'balance_amount' => $balanceAmount,
];
}
/**
* 应用阶梯折扣
*/
private function applyLadderDiscount($presalePrice, $goodsNum, $ladderConfig, $totalNum)
{
$discount = 1.0;
// 按阶梯人数降序排列,匹配第一个满足条件的档位
usort($ladderConfig, fn($a, $b) => $b['num'] - $a['num']);
foreach ($ladderConfig as $ladder) {
if ($totalNum >= $ladder['num']) {
$discount = $ladder['discount'];
break;
}
}
return round($presalePrice * $goodsNum * $discount, 2);
}
private function getPresaleTotalNum($activityId)
{
// 统计该活动下定金已支付的总件数
return PresaleOrderModel::where('activity_id', $activityId)
->whereIn('status', [2, 3])
->sum('goods_num');
}
}
4.1 尾款支付时间窗口控制
尾款支付窗口由 balance_start_time 和 balance_end_time 两个字段控制;用户支付定金后,在窗口开启前无法支付尾款,窗口关闭后订单自动失效。
php
/**
* 校验尾款支付是否在时间窗口内
*/
public function checkBalanceTimeWindow($presaleOrderId)
{
$presaleOrder = PresaleOrderModel::find($presaleOrderId);
$activity = PresaleActivityModel::find($presaleOrder['activity_id']);
$now = time();
if ($now < $activity['balance_start_time']) {
throw new \Exception('尾款支付尚未开始,开始时间为 '
. date('Y-m-d H:i:s', $activity['balance_start_time']));
}
if ($now > $activity['balance_end_time']) {
throw new \Exception('尾款支付已结束');
}
return true;
}
4.2 尾款自动提醒定时任务
LikeShop 的定时任务在管理后台配置和开关,设置路径是「系统设置 → 系统维护 → 定时任务」;所有任务共用 php think crontab 命令,由 PHP 层面的调度器统一管理,新增任务时只需要在数据库中添加记录。
尾款自动提醒的定时任务设计为:扫描所有状态为“已付定金待付尾款”且尾款支付窗口即将开启或即将关闭的订单,按提醒策略发送通知:
php
class PresaleBalanceRemindCrontab
{
/**
* 尾款支付提醒(每10分钟执行一次)
*/
public function remindBalance()
{
$now = time();
// 场景一:尾款支付窗口即将开启(提前1小时提醒)
$upcomingOrders = PresaleOrderModel::alias('po')
->join('presale_activity pa', 'po.activity_id = pa.id')
->where('po.status', 2)
->where('pa.balance_start_time', '>', $now)
->where('pa.balance_start_time', '<=', $now + 3600)
->where('po.remind_status', 0)
->field('po.id, po.user_id, pa.balance_start_time, pa.balance_end_time')
->select();
foreach ($upcomingOrders as $order) {
MessageService::send(
$order['user_id'],
'预售尾款支付提醒',
'您参与的预售活动尾款支付即将开始,支付时间为 '
. date('Y-m-d H:i:s', $order['balance_start_time'])
. ' 至 ' . date('Y-m-d H:i:s', $order['balance_end_time'])
. ',请及时支付尾款。'
);
PresaleOrderModel::where('id', $order['id'])
->update(['remind_status' => 1]);
}
// 场景二:尾款支付窗口即将关闭(提前24小时和2小时各提醒一次)
$expiringOrders = PresaleOrderModel::alias('po')
->join('presale_activity pa', 'po.activity_id = pa.id')
->where('po.status', 2)
->where('pa.balance_end_time', '>', $now)
->where('pa.balance_end_time', '<=', $now + 86400)
->field('po.id, po.user_id, pa.balance_end_time')
->select();
foreach ($expiringOrders as $order) {
$hoursLeft = round(($order['balance_end_time'] - $now) / 3600);
MessageService::send(
$order['user_id'],
'尾款支付即将截止',
"您的预售订单尾款支付还剩 {$hoursLeft} 小时截止,"
. "截止时间为 " . date('Y-m-d H:i:s', $order['balance_end_time'])
. ",逾期订单将自动取消,定金不予退还。"
);
}
}
}
4.3 尾款超时自动关闭任务
尾款支付窗口关闭后,未支付尾款的预售订单自动关闭;根据 deposit_refundable 配置决定是否退还定金:
php
class PresaleBalanceExpireCrontab
{
/**
* 尾款超时自动关闭(每分钟执行)
*/
public function closeExpiredOrders()
{
$now = time();
$expiredOrders = PresaleOrderModel::where('status', 2)
->where('balance_expire_time', '<=', $now)
->where('balance_expire_time', '>', 0)
->limit(50)
->select();
foreach ($expiredOrders as $order) {
Db::startTrans();
try {
$order = PresaleOrderModel::lock(true)->find($order['id']);
if ($order['status'] != 2) {
Db::rollback();
continue;
}
$activity = PresaleActivityModel::find($order['activity_id']);
// 关闭预售订单
PresaleOrderModel::where('id', $order['id'])->update([
'status' => 4,
'update_time' => $now,
]);
// 释放预售库存(如果定金阶段已扣减)
if ($activity['stock_deduct_type'] == 1) {
PresaleActivityModel::where('id', $activity['id'])
->inc('presale_stock', $order['goods_num'])->update();
}
// 根据配置决定是否退还定金
if ($activity['deposit_refundable'] == 1 && $order['deposit_pay_time'] > 0) {
RefundService::refundDeposit(
$order['deposit_order_id'],
$order['deposit_amount'],
'尾款超时未支付,定金退还'
);
}
// 发送通知
MessageService::send(
$order['user_id'],
'预售订单已关闭',
'您的预售订单尾款支付已超时,订单已自动关闭。'
. ($activity['deposit_refundable'] ? '定金已退还至原支付账户。' : '')
);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
Log::error("预售尾款超时关闭失败 order_id={$order['id']}: " . $e->getMessage());
}
}
}
}
5.1 尾款支付流程
尾款支付在预售订单状态为“已付定金待付尾款”时触发,金额计算完成后生成尾款订单:
php
public function payBalance($presaleOrderId, $userId, $couponId = 0, $useIntegral = 0)
{
$presaleOrder = PresaleOrderModel::where('id', $presaleOrderId)
->where('user_id', $userId)->find();
if (!$presaleOrder || $presaleOrder['status'] != 2) {
throw new \Exception('预售订单状态不正确');
}
// 校验尾款支付时间窗口
$this->checkBalanceTimeWindow($presaleOrderId);
// 计算尾款金额
$amountResult = $this->calcBalanceAmount($presaleOrderId, $couponId, $useIntegral);
Db::startTrans();
try {
// 创建尾款订单
$balanceOrderSn = generateSn('PB');
$balanceOrderId = OrderModel::create([
'order_sn' => $balanceOrderSn,
'user_id' => $userId,
'order_type' => 7, // 7-预售尾款订单
'order_amount' => $amountResult['balance_amount'],
'goods_amount' => $amountResult['ladder_price'],
'order_status' => 0,
'pay_status' => 0,
'create_time' => time(),
])->id;
PresaleOrderModel::where('id', $presaleOrderId)->update([
'balance_order_id' => $balanceOrderId,
'balance_order_sn' => $balanceOrderSn,
'balance_amount' => $amountResult['balance_amount'],
]);
Db::commit();
return ['balance_order_sn' => $balanceOrderSn, 'amount' => $amountResult];
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
尾款订单支付成功后,通过事件监听器更新预售订单状态并触发后续流程(尾款阶段实扣库存、更新商品实时库存、进入发货流程):
php
class PresaleBalancePaidListener
{
public function handle($orderId)
{
$order = OrderModel::find($orderId);
if (!$order || $order['order_type'] != 7) return;
$presaleOrder = PresaleOrderModel::where('balance_order_id', $orderId)->find();
if (!$presaleOrder) return;
Db::startTrans();
try {
PresaleOrderModel::where('id', $presaleOrder['id'])->update([
'status' => 3,
'balance_pay_time' => time(),
'balance_paid_amount' => $order['order_amount'],
]);
$activity = PresaleActivityModel::find($presaleOrder['activity_id']);
// 如果配置为尾款阶段实扣库存
if ($activity['stock_deduct_type'] == 2) {
// 扣减预售专属库存
PresaleActivityModel::where('id', $activity['id'])
->dec('presale_stock', $presaleOrder['goods_num'])->update();
// 扣减商品实际库存
GoodsItemModel::where('id', $activity['item_id'])
->dec('stock', $presaleOrder['goods_num'])->update();
}
// 更新主订单的可发货状态(待发货)
// 发货时间由 delivery_time 字段控制
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
}
5.2 发货时间自定义
预售商品的发货时间由 ls_presale_activity.delivery_time 字段控制;支持两种配置模式:
方式一:固定时间发货。设置一个具体的发货时间(如“2026年11月15日”),所有预售订单统一在指定时间后进入发货流程。
方式二:尾款支付后N天发货。设置尾款支付后N天发货,发货时间 = 尾款支付时间 + N 天。
在发货逻辑中,定时任务扫描已付尾款且到达发货时间的预售订单,将其推入待发货队列:
php
class PresaleDeliveryCrontab
{
/**
* 预售发货时间到达后自动进入待发货流程(每30分钟执行)
*/
public function checkDeliveryTime()
{
$now = time();
$readyOrders = PresaleOrderModel::where('status', 3)
->where('delivery_time', '<=', $now)
->where('delivery_time', '>', 0)
->limit(100)
->select();
foreach ($readyOrders as $order) {
// 更新关联的尾款订单状态为待发货
if ($order['balance_order_id'] > 0) {
OrderModel::where('id', $order['balance_order_id'])
->where('order_status', 1)
->update(['order_status' => 2]); // 2-待发货
// 通知商家发货
MessageService::sendToShop(
$order['goods_id'],
'预售订单待发货',
"预售订单 {$order['balance_order_sn']} 已到达发货时间,请及时处理。"
);
}
}
}
}
6.1 后台配置清单
在“营销 → 预售活动”中增加活动配置页面:
| 配置项 | 类型 | 说明 |
|---|---|---|
| 活动名称 | 文本输入 | |
| 预售商品 | 商品选择器 | 支持选择商品和规格 |
| 预售价 | 金额输入 | |
| 定金金额 | 金额输入 | |
| 定金类型 | 单选 | 定金膨胀 / 定金立减 |
| 定金膨胀金额 | 金额输入 | 定金类型为膨胀时填写,必须≥定金金额 |
| 定金立减金额 | 金额输入 | 定金类型为立减时填写 |
| 预售专属库存 | 数字输入 | 独立于商品常规库存 |
| 库存扣减时机 | 单选 | 付定金时预扣 / 付尾款时实扣 |
| 定金支付时间 | 日期时间范围 | 开始时间 / 结束时间 |
| 尾款支付时间 | 日期时间范围 | 开始时间 / 结束时间 |
| 预计发货时间 | 日期时间选择 | 固定时间或尾款后N天 |
| 阶梯优惠 | 开关+配置 | 满N件打X折,支持多档位 |
| 限购数量 | 数字输入 | 0为不限 |
| 定金是否可退 | 开关 | 尾款超时后是否退还定金 |
6.2 开发注意事项
定金订单的地址选择时机。参考行业实践,定金订单只是选择规则付款生成,不选择下单地址等参数,当支付尾款的时候才选择正常的下单参数;这意味着预售订单在定金阶段不需要收货地址,尾款支付阶段才收集地址信息。
定金与优惠券的兼容规则。优惠券门槛的计算按“售价 x 阶梯折扣 - 定金膨胀金”进行,不使用会员价计算;这意味着优惠券可用性判断发生在尾款支付阶段,而非定金阶段。
库存隔离的核心原则。预售专属库存(presale_stock)与商品常规库存(ls_goods_item.stock)必须物理隔离;在多商户场景下,库存扣减必须按商户维度隔离,不能出现A商户的库存被B商户的用户扣减的情况;如果采用定金阶段和策略,需要确保预扣操作在独立的事务和行锁保护下完成。
定金不可直接退的约束。用户要退回定金需要付尾款后申请退货,那样定金也就一并退了;或者联系商家私下退款定金(后台操作);二开中应保留这一约束,同时在后台提供商家手动退款定金的入口。
定时任务的依赖关系。尾款提醒、尾款超时关闭、预售发货三个定时任务存在时序依赖:提醒任务在尾款窗口期间执行,超时关闭任务在窗口关闭后执行,发货任务在尾款支付且到达发货时间执行;LikeShop 的定时任务主要处理订单退款扫描、关闭超时未支付订单、佣金自动结算、活动自动开启/结束等场景;预售相关的定时任务作为新增子任务接入,需要注意与已有任务的执行时间错开,避免资源竞争。
