From 04fc087ae765acfb8fff6e204524fd07c1898d8c Mon Sep 17 00:00:00 2001 From: wikiboss Date: Sat, 12 Sep 2026 17:07:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=8D=E8=B4=AD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=E6=8C=87=E5=AE=9A=E5=95=86=E5=93=81=E5=A4=8D?= =?UTF-8?q?=E8=B4=AD=E4=BB=98=E6=AC=BE=E6=97=B6=E7=9B=B4=E6=8E=A5=E7=AB=8B?= =?UTF-8?q?=E5=87=8F=EF=BC=88=E4=B8=8D=E8=BF=94=E7=8E=B0=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E8=A7=A3=E8=80=A6=E5=88=86=E9=94=80=E4=B9=B0=E5=AE=B6=E4=BD=A3?= =?UTF-8?q?=E9=87=91=EF=BC=8C=E7=BA=AF=E5=90=8E=E7=AB=AF=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopro/library/commission/Commission.php | 141 ++------- .../library/commission/RepurchaseService.php | 206 +++++++++++++ .../library/traits/model/order/OrderOper.php | 51 +++- .../traits/model/order/OrderOperCreate.php | 37 ++- addons/shopro/model/Activity.php | 6 +- addons/shopro/model/Order.php | 7 +- .../tests/CommissionScenarioCalculator.php | 36 +-- .../shopro/tests/repurchase_discount_test.php | 270 ++++++++++++++++++ .../view/shopro/commission/config/index.html | 12 +- .../view/shopro/commission/goods/edit.html | 4 +- 10 files changed, 595 insertions(+), 175 deletions(-) create mode 100644 addons/shopro/library/commission/RepurchaseService.php create mode 100644 addons/shopro/tests/repurchase_discount_test.php diff --git a/addons/shopro/library/commission/Commission.php b/addons/shopro/library/commission/Commission.php index 86cdc63..7d5a4ea 100644 --- a/addons/shopro/library/commission/Commission.php +++ b/addons/shopro/library/commission/Commission.php @@ -128,11 +128,8 @@ class Commission - $hasRepurchaseSelf = $this->isRepurchase() - && $this->_config->isRepurchaseSelfEnable() - && $this->goods->isRepurchaseEnable(); - - + // 复购已在下单付款时直接减免,分销佣金奖池中不再发放买家本人复购返现 + $hasRepurchaseSelf = false; if (!$hasChainCommission && !$hasTierCommission && !$hasRepurchaseSelf) { @@ -677,135 +674,29 @@ class Commission protected function runRepurchaseRewards($commissionOrder) - { - - if (!$this->isRepurchase() || !$this->_config->isRepurchaseSelfEnable() || !$this->goods->isRepurchaseEnable()) { - - return; - - } - - - - $commission = 0; - - $rule = ['award_type' => 'repurchase_self']; - - if ($this->_config->getRepurchaseSelfType() === 'fixed') { - - $commission = round($this->_config->getRepurchaseSelfAmount(), 2); - - $rule['type'] = 'fixed'; - - $rule['amount'] = $commission; - - } else { - - $rate = $this->_config->getRepurchaseSelfRate(); - - if ($rate <= 0) { - - return; - - } - - $commission = round($this->amount * $rate * 0.01, 2); - - $rule['type'] = 'rate'; - - $rule['rate'] = $rate; - - } - - - - if ($commission <= 0) { - - return; - - } - - - - $rewards = RewardModel::where('commission_order_id', $commissionOrder->id) - - ->where('agent_id', $this->buyer->user->id) - - ->where('commission_level', 0) - - ->select(); - - foreach ($rewards as $reward) { - - $rewardRules = json_decode($reward->commission_rules, true); - - if (($rewardRules['award_type'] ?? '') === 'repurchase_self') { - - return; - - } - - } - - - - $reward = RewardModel::create([ - - 'order_id' => $commissionOrder->order_id, - - 'order_item_id' => $commissionOrder->order_item_id, - - 'buyer_id' => $commissionOrder->buyer_id, - - 'commission_order_id' => $commissionOrder->id, - - 'agent_id' => $this->buyer->user->id, - - 'type' => 'money', - - 'commission' => $commission, - - 'status' => 0, - - 'original_commission' => $commission, - - 'commission_level' => 0, - - 'commission_rules' => json_encode($rule), - - 'agent_level' => 0, - - ]); - - - - Log::write($this->buyer->user->id, 'reward', [ - - 'action' => 'repurchase_self', - - 'reward' => $reward, - - ], ['oper_type' => 'user', 'oper_id' => $commissionOrder->buyer_id]); - + // 复购优惠已在商品下单付款时直接立减,此处不再发放买家本人复购返现佣金 + return; } - - protected function isRepurchase() - { + $goodsId = $this->item['goods_id'] ?? 0; + $userId = $this->buyer->user->id ?? 0; + if (!$goodsId || !$userId) { + return false; + } - $count = \addons\shopro\model\Order::where('user_id', $this->buyer->user->id) - - ->where('status', 'in', [1, 2]) - - ->where('id', '<', $this->item['order_id']) - + $count = \addons\shopro\model\OrderItem::alias('oi') + ->join('shopro_order o', 'o.id = oi.order_id') + ->where('o.user_id', $userId) + ->where('o.status', 'in', [1, 2]) + ->where('oi.goods_id', $goodsId) + ->where('o.id', '<', $this->item['order_id']) + ->where('oi.refund_status', '<=', 0) ->count(); return $count > 0; - } diff --git a/addons/shopro/library/commission/RepurchaseService.php b/addons/shopro/library/commission/RepurchaseService.php new file mode 100644 index 0000000..2d3f579 --- /dev/null +++ b/addons/shopro/library/commission/RepurchaseService.php @@ -0,0 +1,206 @@ + $configModel->isRepurchaseEnable(), + 'self_enable' => $configModel->isRepurchaseSelfEnable(), + 'self_type' => $configModel->getRepurchaseSelfType(), + 'self_amount' => $configModel->getRepurchaseSelfAmount(), + 'self_rate' => $configModel->getRepurchaseSelfRate(), + ]; + } catch (\Throwable $e) { + $config = [ + 'enable' => false, + 'self_enable' => false, + 'self_type' => 'fixed', + 'self_amount' => 0.0, + 'self_rate' => 0.0, + ]; + } + + return $config; + } + + /** + * 检查商品是否开启复购规则 + * + * @param int $goodsId + * @return bool + */ + public static function isGoodsRepurchaseEnable($goodsId) + { + if (!$goodsId) { + return false; + } + + try { + $row = Db::name('shopro_commission_goods') + ->where('goods_id', $goodsId) + ->where('status', 1) + ->field('repurchase_enable') + ->find(); + return !empty($row['repurchase_enable']); + } catch (\Throwable $e) { + return false; + } + } + + /** + * 检查用户是否曾购买过该指定商品(有效付款订单) + * + * @param int $userId + * @param int $goodsId + * @return bool + */ + public static function isUserRepurchasedGoods($userId, $goodsId) + { + if (!$userId || !$goodsId) { + return false; + } + + try { + $count = Db::name('shopro_order_item') + ->alias('oi') + ->join('shopro_order o', 'o.id = oi.order_id') + ->where('o.user_id', $userId) + ->where('o.status', 'in', [1, 2]) + ->where('oi.goods_id', $goodsId) + ->where('oi.refund_status', '<=', 0) + ->count(); + + return $count > 0; + } catch (\Throwable $e) { + return false; + } + } + + /** + * 计算单项商品的复购立减金额 + * + * @param array $item 商品信息 (goods_id, goods_num, goods_price, goods_amount) + * @param array $config 复购配置 + * @param bool $isGoodsEnable 该商品是否开启复购 + * @param bool $isUserRepurchase 该用户是否复购该商品 + * @return float + */ + public static function calculateItemDiscount($item, $config, $isGoodsEnable, $isUserRepurchase) + { + if (empty($config['enable']) || empty($config['self_enable'])) { + return 0.0; + } + + if (!$isGoodsEnable || !$isUserRepurchase) { + return 0.0; + } + + $goodsNum = isset($item['goods_num']) ? intval($item['goods_num']) : 1; + $goodsAmount = isset($item['goods_amount']) ? floatval($item['goods_amount']) : 0.0; + $goodsPrice = isset($item['goods_price']) ? floatval($item['goods_price']) : ($goodsNum > 0 ? $goodsAmount / $goodsNum : 0.0); + + if ($goodsAmount <= 0) { + return 0.0; + } + + $discount = 0.0; + $selfType = $config['self_type'] ?? 'fixed'; + + if ($selfType === 'rate') { + $rate = floatval($config['self_rate'] ?? 0); + if ($rate > 0) { + $discount = round($goodsAmount * $rate * 0.01, 2); + } + } else { + // 固定金额模式:按件数累加立减 + $unitAmount = floatval($config['self_amount'] ?? 0); + if ($unitAmount > 0) { + // 单件立减不可超过单件售价 + $validUnitDiscount = min($unitAmount, $goodsPrice); + $discount = round($validUnitDiscount * $goodsNum, 2); + } + } + + // 兜底保护:总立减金额不可超过商品总额 + if ($discount > $goodsAmount) { + $discount = $goodsAmount; + } + + return max(0.0, $discount); + } + + /** + * 批量计算订单中各商品的复购立减金额 + * + * @param array $goodsList 商品列表 + * @param int $userId 用户ID + * @param array|null $config 配置 + * @param callable|null $goodsRuleChecker 自定义商品规则检查器 (用于单测或依赖注入) + * @param callable|null $historyChecker 自定义历史购买检查器 (用于单测或依赖注入) + * @return array ['goods_list' => ..., 'total_repurchase_discount' => float] + */ + public static function calculateOrderRepurchaseDiscounts( + $goodsList, + $userId, + $config = null, + $goodsRuleChecker = null, + $historyChecker = null + ) { + if ($config === null) { + $config = self::getConfig(); + } + + $totalDiscount = 0.0; + + foreach ($goodsList as $key => $item) { + $goodsId = $item['goods_id'] ?? 0; + + // 检查商品是否参与复购 + $isGoodsEnable = false; + if ($goodsRuleChecker !== null) { + $isGoodsEnable = (bool) call_user_func($goodsRuleChecker, $goodsId); + } else { + $isGoodsEnable = self::isGoodsRepurchaseEnable($goodsId); + } + + // 检查用户是否复购该商品 + $isUserRepurchase = false; + if ($isGoodsEnable) { + if ($historyChecker !== null) { + $isUserRepurchase = (bool) call_user_func($historyChecker, $userId, $goodsId); + } else { + $isUserRepurchase = self::isUserRepurchasedGoods($userId, $goodsId); + } + } + + $discount = self::calculateItemDiscount($item, $config, $isGoodsEnable, $isUserRepurchase); + + $goodsList[$key]['repurchase_discount_money'] = $discount; + $goodsList[$key]['is_repurchase'] = ($isGoodsEnable && $isUserRepurchase && $discount > 0) ? 1 : 0; + + $totalDiscount = bcadd((string) $totalDiscount, (string) $discount, 2); + } + + return [ + 'goods_list' => $goodsList, + 'total_repurchase_discount' => floatval($totalDiscount) + ]; + } +} diff --git a/addons/shopro/library/traits/model/order/OrderOper.php b/addons/shopro/library/traits/model/order/OrderOper.php index 7b6722e..32250dc 100644 --- a/addons/shopro/library/traits/model/order/OrderOper.php +++ b/addons/shopro/library/traits/model/order/OrderOper.php @@ -88,6 +88,41 @@ trait OrderOper $user_address ); + // 计算满足复购规则的指定商品的复购立减 + $repurchase_discount_money = 0.0; + try { + $repurchaseResult = \addons\shopro\library\commission\RepurchaseService::calculateOrderRepurchaseDiscounts( + $new_goods_list, + $user ? $user->id : 0 + ); + $new_goods_list = $repurchaseResult['goods_list']; + $repurchase_discount_money = $repurchaseResult['total_repurchase_discount']; + + // 将复购立减注入活动优惠列表,前端零改动原生展示 + if ($repurchase_discount_money > 0) { + $repurchaseGoodsIds = []; + foreach ($new_goods_list as $g) { + if (!empty($g['repurchase_discount_money']) && $g['repurchase_discount_money'] > 0) { + $repurchaseGoodsIds[] = $g['goods_id']; + } + } + $activity_discount_infos[] = [ + 'activity_id' => 'repurchase', + 'activity_title' => '复购立减', + 'activity_type' => 'repurchase', + 'activity_discount_money' => $repurchase_discount_money, + 'rule_type' => 'money', + 'discount_rule' => [ + 'discount' => $repurchase_discount_money, + ], + 'goods_ids' => implode(',', $repurchaseGoodsIds) + ]; + $activity_discount_money = bcadd((string)$activity_discount_money, (string)$repurchase_discount_money, 2); + } + } catch (\Throwable $e) { + $repurchase_discount_money = 0.0; + } + // 计算订单总金额,需支付金额 list( $new_goods_list, @@ -105,7 +140,8 @@ trait OrderOper $dispatch_discount_money, $free_shipping_goods_ids, $coupon_money, - $deliveryFee + $deliveryFee, + $repurchase_discount_money ); // 获取发票金额 @@ -150,7 +186,8 @@ trait OrderOper $invoice_amount, $deliveryFee, $expressList, - $expressName + $expressName, + $repurchase_discount_money ); } @@ -242,7 +279,8 @@ trait OrderOper $invoice, $invoice_amount, $deliveryFee, - $expressName + $expressName, + $repurchase_discount_money ) { // 订单创建前 $data = [ @@ -260,6 +298,7 @@ trait OrderOper 'coupon_fee' => $coupon_fee, 'activity_discount_money' => $activity_discount_money, 'dispatch_discount_money' => $dispatch_discount_money, + 'repurchase_discount_money' => $repurchase_discount_money, 'activity_discount_infos' => $activity_discount_infos, 'new_goods_list' => $new_goods_list, 'activity_type' => $activity_type, @@ -348,6 +387,9 @@ trait OrderOper $orderData['platform'] = request()->header('platform'); $ext = $activity_discount_infos ? ['activity_discount_infos' => $activity_discount_infos] : []; // 促销活动信息 + if (!empty($repurchase_discount_money) && floatval($repurchase_discount_money) > 0) { + $ext['repurchase_discount_money'] = $repurchase_discount_money; + } $orderData['ext'] = json_encode($ext); $order = new Order(); $order->allowField(true)->save($orderData); @@ -428,6 +470,9 @@ trait OrderOper if (isset($buyinfo['dispatch_phone'])) { $ext['dispatch_phone'] = $buyinfo['dispatch_phone']; } + if (isset($buyinfo['repurchase_discount_money']) && floatval($buyinfo['repurchase_discount_money']) > 0) { + $ext['repurchase_discount_money'] = $buyinfo['repurchase_discount_money']; + } $orderItem->ext = json_encode($ext); $orderItem->save(); } diff --git a/addons/shopro/library/traits/model/order/OrderOperCreate.php b/addons/shopro/library/traits/model/order/OrderOperCreate.php index cf4f0bd..ad04155 100644 --- a/addons/shopro/library/traits/model/order/OrderOperCreate.php +++ b/addons/shopro/library/traits/model/order/OrderOperCreate.php @@ -717,7 +717,8 @@ trait OrderOperCreate $dispatch_discount_money, $free_shipping_goods_ids, $coupon_money, - $deliveryFee + $deliveryFee, + $repurchase_discount_money = 0 ) { // (搜这个找注释)加上运费,dispatch_amount 没有重新计算,和 origin_dispatch_amount 相同 @@ -737,7 +738,8 @@ trait OrderOperCreate $activity_discount_infos, $activity_discount_money, $dispatch_discount_money, - $free_shipping_goods_ids + $free_shipping_goods_ids, + $repurchase_discount_money ); return [ @@ -814,7 +816,8 @@ trait OrderOperCreate $invoice_amount, $deliveryFee, $expressList, - $expressName + $expressName, + $repurchase_discount_money = 0 ) { // 需要处理小数点的数据 @@ -829,6 +832,7 @@ trait OrderOperCreate "coupon_fee", "activity_discount_money", "dispatch_discount_money", + "repurchase_discount_money", "invoice_amount", 'deliveryFee' ); @@ -882,7 +886,8 @@ trait OrderOperCreate $activity_discount_infos, $activity_discount_money, $dispatch_discount_money, - $free_shipping_goods_ids = [] + $free_shipping_goods_ids = [], + $repurchase_discount_money = 0 ) { // 如果邮费有优惠,分配邮费优惠的金额 @@ -915,8 +920,8 @@ trait OrderOperCreate if (floatval($activity_discount_money)) { foreach ($activity_discount_infos as $key => $info) { - if ($info['activity_type'] == 'free_shipping') { - // 上面已经单独处理过满包邮优惠 + if ($info['activity_type'] == 'free_shipping' || $info['activity_type'] == 'repurchase') { + // 上面已经单独处理过满包邮优惠,复购立减优先按商品精准分配 continue; } $current_discount_goods_ids = explode(',', $info['goods_ids']); @@ -946,8 +951,21 @@ trait OrderOperCreate } } - // 剩余要加权平均分配的优惠 - $last_discount_money = bcsub($discount_fee, bcadd($dispatch_discount_money, $activity_discount_money, 2), 2); + // 如果存在复购立减,精准归属到满足复购的指定商品本身 + if (floatval($repurchase_discount_money)) { + foreach ($new_goods_list as $key => $buyinfo) { + $itemRepurchaseDiscount = $buyinfo['repurchase_discount_money'] ?? 0; + if (floatval($itemRepurchaseDiscount) > 0) { + $new_goods_list[$key]['discount_fee'] = bcadd($new_goods_list[$key]['discount_fee'], (string)$itemRepurchaseDiscount, 2); + } + } + } + + // 剩余要加权平均分配的优惠(如优惠券),需剔除邮费优惠和全部活动优惠(含复购立减) + $assigned_discount = bcadd($dispatch_discount_money, $activity_discount_money, 2); + $last_discount_money = bcsub($discount_fee, $assigned_discount, 2); + $last_discount_money = $last_discount_money > 0 ? $last_discount_money : 0; + // 计算每个商品分配到的优惠券的优惠金额,顺便计算出来 pay_price 的金额 foreach ($new_goods_list as $key => $buyinfo) { $scale = 0; // 按照商品价格和总价计算每个 item 的比例 @@ -959,7 +977,8 @@ trait OrderOperCreate $current_discount_fee = round(bcmul($last_discount_money, $scale, 3), 2); $new_goods_list[$key]['discount_fee'] = bcadd($new_goods_list[$key]['discount_fee'], $current_discount_fee, 2); // 每个商品除了运费之后分配的支付金额 - $new_goods_list[$key]['pay_price'] = bcsub($buyinfo['goods_amount'], $new_goods_list[$key]['discount_fee'], 2); + $calc_pay_price = bcsub($buyinfo['goods_amount'], $new_goods_list[$key]['discount_fee'], 2); + $new_goods_list[$key]['pay_price'] = $calc_pay_price < 0 ? '0.00' : $calc_pay_price; } return $new_goods_list; diff --git a/addons/shopro/model/Activity.php b/addons/shopro/model/Activity.php index c096bdf..46f3d9c 100644 --- a/addons/shopro/model/Activity.php +++ b/addons/shopro/model/Activity.php @@ -32,7 +32,7 @@ class Activity extends Model public static function getTypeList() { - return ['seckill' => '秒杀', 'timer' => '限时秒杀', 'robbuy' => '预约抢购', 'groupon' => '拼团', 'full_reduce' => '满额立减', 'full_discount' => '满额折扣', 'free_shipping' => '满额包邮']; + return ['seckill' => '秒杀', 'timer' => '限时秒杀', 'robbuy' => '预约抢购', 'groupon' => '拼团', 'full_reduce' => '满额立减', 'full_discount' => '满额折扣', 'free_shipping' => '满额包邮', 'repurchase' => '复购立减']; } /** @@ -49,9 +49,11 @@ class Activity extends Model $tag .= $activity_type == 'full_reduce' ? '减' . $ruleData['discount'] : $ruleData['discount'] . '折'; } else if ($activity_type == 'free_shipping') { $tag = '满' . $ruleData['full_num'] . ($ruleData['type'] == 'money' ? '元' : '件') . '包邮'; + } else if ($activity_type == 'repurchase') { + $tag = '老客复购立减' . ($ruleData['discount'] ?? '') . '元'; } - return $tag; + return $tag ?? ''; } diff --git a/addons/shopro/model/Order.php b/addons/shopro/model/Order.php index db0c875..e5344fb 100644 --- a/addons/shopro/model/Order.php +++ b/addons/shopro/model/Order.php @@ -55,10 +55,11 @@ class Order extends Model if ($ext && isset($ext['activity_discount_infos']) && $ext['activity_discount_infos']) { foreach ($ext['activity_discount_infos'] as $key => $info) { - $ext['activity_discount_infos'][$key]['activity_type_text'] = Activity::getTypeList()[$info['activity_type']]; + $typeList = Activity::getTypeList(); + $ext['activity_discount_infos'][$key]['activity_type_text'] = $typeList[$info['activity_type']] ?? ($info['activity_title'] ?? '优惠'); $ext['activity_discount_infos'][$key]['format_text'] = Activity::formatDiscountTags($info['activity_type'], array_merge([ - 'type' => $info['rule_type'], - ], $info['discount_rule'])); + 'type' => $info['rule_type'] ?? 'money', + ], $info['discount_rule'] ?? [])); } } return $ext; diff --git a/addons/shopro/tests/CommissionScenarioCalculator.php b/addons/shopro/tests/CommissionScenarioCalculator.php index 4e9d2c5..9ea2622 100644 --- a/addons/shopro/tests/CommissionScenarioCalculator.php +++ b/addons/shopro/tests/CommissionScenarioCalculator.php @@ -72,22 +72,8 @@ class CommissionScenarioCalculator } } - if ($isRepurchase && $repurchaseSelfEnable && $productRepurchaseEnable) { - $cashback = 0; - if ($repurchaseSelfType === 'fixed') { - $cashback = round($repurchaseSelfAmount, 2); - } elseif ($repurchaseSelfRate > 0) { - $cashback = round($amount * $repurchaseSelfRate * 0.01, 2); - } - if ($cashback > 0) { - $rewards[] = [ - 'name' => $buyerName, - 'award_type' => 'repurchase_self', - 'commission_level' => 0, - 'commission' => $cashback, - ]; - } - } + // 复购已优化为付款时直接减免,分销模块不再向买家发放事后返现佣金 + // if ($isRepurchase && $repurchaseSelfEnable && $productRepurchaseEnable) { ... } $total = 0; foreach ($rewards as $row) { @@ -248,13 +234,13 @@ class CommissionScenarioCalculator ], ], 'scenario_3' => [ - 'title' => '场景3:复购 · D 第二次购买1000元 · 本人固定返现5元', + 'title' => '场景3:复购 · D 第二次购买1000元 · 付款时已立减5元(不返现)', 'steps' => [ - '1. D 已有至少一笔 status in (1,2) 的历史订单', - '2. 复购推荐人分佣=继续分佣;本人复购返现=开启,类型=固定5元', - '3. 商品勾选参与复购返现', - '4. D 再次下单 1000 元支付', - '5. 推荐人仍拿关系链+级差+管理奖;D 本人额外拿 5 元复购返现', + '1. D 历史已有至少一笔包含该指定商品的有效订单 (status in 1,2)', + '2. 复购推荐人分佣=继续分佣;本人复购立减=开启,类型=固定5元', + '3. 商品勾选参与复购立减', + '4. D 再次下单时直接立减 5 元,实付 995 元', + '5. 推荐人仍拿关系链+级差+管理奖;D 本人付款立减,不再产生返现佣金', ], 'options' => [ 'amount' => 1000, @@ -268,11 +254,11 @@ class CommissionScenarioCalculator ], ], 'scenario_4' => [ - 'title' => '场景4:复购 · 推荐人不再分佣 · 仅本人返现5元', + 'title' => '场景4:复购 · 推荐人不再分佣 · 买家付款立减5元(佣金奖池为0)', 'steps' => [ '1. 分销设置:复购推荐人分佣=不再分佣', - '2. D 复购 1000 元', - '3. 预期:C/B/A 无佣金,仅 D 得 5 元复购返现', + '2. D 复购付款时已直接立减 5 元', + '3. 预期:C/B/A 无分销佣金,买家已立减亦无返现佣金,总佣金为 0', ], 'options' => [ 'amount' => 1000, diff --git a/addons/shopro/tests/repurchase_discount_test.php b/addons/shopro/tests/repurchase_discount_test.php new file mode 100644 index 0000000..0e7d1f2 --- /dev/null +++ b/addons/shopro/tests/repurchase_discount_test.php @@ -0,0 +1,270 @@ +testNonRepurchaseGoods(); + $this->testFirstBuyNoDiscount(); + $this->testRepurchaseFixedDiscount(); + $this->testRepurchaseRateDiscount(); + $this->testDiscountCapProtection(); + $this->testMixedOrderDiscounts(); + + echo "\n----------------------------------------\n"; + echo "测试结果: {$this->passed} 通过, {$this->failed} 失败\n"; + echo "----------------------------------------\n"; + + if ($this->failed > 0) { + exit(1); + } + } + + private function assert($condition, $message) + { + if ($condition) { + $this->passed++; + echo " [PASS] {$message}\n"; + } else { + $this->failed++; + echo " [FAIL] {$message}\n"; + } + } + + /** + * 测试场景 1: 商品未开启复购规则,即使买家买过多次,也不享受复购立减 + */ + public function testNonRepurchaseGoods() + { + echo "场景 1: 未开启复购的普通商品测试\n"; + + $item = [ + 'goods_id' => 101, + 'goods_num' => 2, + 'goods_price' => 100.00, + 'goods_amount' => 200.00 + ]; + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'fixed', + 'self_amount' => 10.00, + 'self_rate' => 5.0 + ]; + + // 模拟商品没有开启复购规则 (repurchase_enable = 0) + $discount = RepurchaseService::calculateItemDiscount( + $item, + $config, + false, // 商品未参与复购 + true // 买家历史买过 + ); + + $this->assert($discount === 0.0, "未开启复购的商品立减金额应为 0.00 (实际: {$discount})"); + } + + /** + * 测试场景 2: 指定商品首次购买(历史无订单),不享受立减 + */ + public function testFirstBuyNoDiscount() + { + echo "\n场景 2: 指定商品首购(历史无该商品购买记录)\n"; + + $item = [ + 'goods_id' => 102, + 'goods_num' => 1, + 'goods_price' => 200.00, + 'goods_amount' => 200.00 + ]; + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'fixed', + 'self_amount' => 20.00, + 'self_rate' => 10.0 + ]; + + // 商品参与复购,但买家首次购买(isRepurchase = false) + $discount = RepurchaseService::calculateItemDiscount( + $item, + $config, + true, // 商品参与复购 + false // 买家首次购买 + ); + + $this->assert($discount === 0.0, "首购指定商品立减金额应为 0.00 (实际: {$discount})"); + } + + /** + * 测试场景 3: 指定商品复购(固定金额模式,按件累加) + */ + public function testRepurchaseFixedDiscount() + { + echo "\n场景 3: 指定商品复购 · 固定金额模式 (买 3 件,每件立减 5 元)\n"; + + $item = [ + 'goods_id' => 103, + 'goods_num' => 3, + 'goods_price' => 50.00, + 'goods_amount' => 150.00 + ]; + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'fixed', + 'self_amount' => 5.00, + 'self_rate' => 0 + ]; + + $discount = RepurchaseService::calculateItemDiscount( + $item, + $config, + true, // 商品参与复购 + true // 买家复购 + ); + + // 预期立减 5 * 3 = 15.00 + $this->assert($discount === 15.00, "复购 3 件每件立减 5 元,总立减应为 15.00 (实际: {$discount})"); + } + + /** + * 测试场景 4: 指定商品复购 · 百分比模式 (立减 10%) + */ + public function testRepurchaseRateDiscount() + { + echo "\n场景 4: 指定商品复购 · 百分比模式 (100 元 × 2 件,立减 10%)\n"; + + $item = [ + 'goods_id' => 104, + 'goods_num' => 2, + 'goods_price' => 100.00, + 'goods_amount' => 200.00 + ]; + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'rate', + 'self_amount' => 0, + 'self_rate' => 10.0 + ]; + + $discount = RepurchaseService::calculateItemDiscount( + $item, + $config, + true, // 商品参与复购 + true // 买家复购 + ); + + // 预期立减 200 * 10% = 20.00 + $this->assert($discount === 20.00, "复购 200 元立减 10%,总立减应为 20.00 (实际: {$discount})"); + } + + /** + * 测试场景 5: 边界兜底保护(立减金额不可超过商品总额) + */ + public function testDiscountCapProtection() + { + echo "\n场景 5: 边界兜底保护 (商品单价 5 元,配置立减 10 元)\n"; + + $item = [ + 'goods_id' => 105, + 'goods_num' => 1, + 'goods_price' => 5.00, + 'goods_amount' => 5.00 + ]; + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'fixed', + 'self_amount' => 10.00, + 'self_rate' => 0 + ]; + + $discount = RepurchaseService::calculateItemDiscount( + $item, + $config, + true, // 商品参与复购 + true // 买家复购 + ); + + // 预期立减不得超过 5.00 + $this->assert($discount === 5.00, "立减金额不得超过商品金额 5.00 (实际: {$discount})"); + } + + /** + * 测试场景 6: 混合商品订单结算 (包含复购商品与非复购商品) + */ + public function testMixedOrderDiscounts() + { + echo "\n场景 6: 混合商品订单结算 (商品A复购立减10元,商品B首购不减)\n"; + + $goodsList = [ + [ + 'goods_id' => 201, + 'goods_num' => 1, + 'goods_price' => 100.00, + 'goods_amount' => 100.00, + 'discount_fee' => 0.00 + ], + [ + 'goods_id' => 202, + 'goods_num' => 2, + 'goods_price' => 50.00, + 'goods_amount' => 100.00, + 'discount_fee' => 0.00 + ] + ]; + + $config = [ + 'enable' => true, + 'self_enable' => true, + 'self_type' => 'fixed', + 'self_amount' => 10.00, + 'self_rate' => 0 + ]; + + // 模拟商品规则:201 参与复购,202 不参与 + $goodsRuleChecker = function ($goodsId) { + return $goodsId === 201; + }; + + // 模拟历史购买记录:买家买过 201,未买过 202 + $historyChecker = function ($userId, $goodsId) { + return $goodsId === 201; + }; + + $result = RepurchaseService::calculateOrderRepurchaseDiscounts( + $goodsList, + 1, // userId + $config, + $goodsRuleChecker, + $historyChecker + ); + + $totalDiscount = $result['total_repurchase_discount']; + $newGoodsList = $result['goods_list']; + + $this->assert($totalDiscount === 10.00, "整单复购立减总额应为 10.00 (实际: {$totalDiscount})"); + $this->assert($newGoodsList[0]['repurchase_discount_money'] === 10.00, "商品 201 立减应为 10.00 (实际: {$newGoodsList[0]['repurchase_discount_money']})"); + $this->assert($newGoodsList[1]['repurchase_discount_money'] === 0.00, "商品 202 立减应为 0.00 (实际: {$newGoodsList[1]['repurchase_discount_money']})"); + } +} + +(new RepurchaseDiscountTest())->run(); diff --git a/application/admin/view/shopro/commission/config/index.html b/application/admin/view/shopro/commission/config/index.html index 2be1be3..b987ded 100644 --- a/application/admin/view/shopro/commission/config/index.html +++ b/application/admin/view/shopro/commission/config/index.html @@ -621,25 +621,25 @@
买家复购时,其上级推荐人是否仍按关系链/级差/管理奖规则获得差价;选「不再分佣」则复购订单不再给推荐人发任何推广差价
- + 开启 关闭 -
买家复购时是否给买家本人发放复购返现;需在分销商品上勾选「参与复购返现」才会对该商品生效
+
买家复购指定商品时是否在商品付款时直接减免(不返现);需在分销商品上勾选「参与复购立减」才会对该指定商品生效
- + 固定金额 订单百分比 - + - + - + diff --git a/application/admin/view/shopro/commission/goods/edit.html b/application/admin/view/shopro/commission/goods/edit.html index f8602c8..407d8fc 100644 --- a/application/admin/view/shopro/commission/goods/edit.html +++ b/application/admin/view/shopro/commission/goods/edit.html @@ -226,14 +226,14 @@ - +
+ content="开启后,买家再次购买该商品时可直接在付款时享受减免(需全局开启「本人复购立减」);关闭则该商品不参与复购减免">