'送货', 1 => '自取', 2 => '跑腿', 3 => '物流', 4 => '快递' ]; for ($style = 0; $style <= 4; $style++) { $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]); $sort = 0; $alias = $aliases[$style]; $status = 1; if (!empty($config)) { $sort = isset($config['sort']) ? (int)$config['sort'] : 0; $alias = !empty($config['name']) ? $config['name'] : $aliases[$style]; $status = isset($config['status']) ? (int)$config['status'] : 1; } $list[] = [ 'style' => $style, 'name' => $alias, 'sort' => $sort, 'status' => $status ]; } return $list; } /** * 获取指定配送方式的配置(不存在则按字典默认初始化) * @param int $shopId 门店ID * @param int $mainId 商户ID * @param int $style 配送类型 0-4 * @return array */ public static function getConfig($shopId, $mainId, $style) { // 复杂分支/关键逻辑:按 mainId + style 查 xhPsMethod,无记录则写入默认配置 $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]); if (empty($config)) { $defaultConfig = dict::getDict('shMethod', $style); $initData = array_merge([ 'shopId' => $shopId, 'mainId' => $mainId, 'style' => $style, ], $defaultConfig); $config = self::add($initData, true); $config = $config->toArray(); } // 关联说明项 xhPsExplain $config['explains'] = PsExplainClass::getAllByCondition( ['methodId' => $config['id']], 'sort ASC, id ASC' ); // 跑腿 style=2 时加载满减规则 xhPsReduceRule if ($style == 2) { $config['reduceRules'] = PsReduceRuleClass::getAllByCondition( ['methodId' => $config['id']], 'sort ASC, id ASC' ); } else { $config['reduceRules'] = []; } return $config; } /** * 客户 homeRule=1 时,用 xhCustom/xhHd 上的送货上门字段覆盖 style=0 配置 * @param array $config xhPsMethod 送货配置 * @param array $customInfo 客户信息(含 home、homeRule、homeAmount 等) * @return array */ public static function applyCustomHomeRule($config, $customInfo) { if (empty($config) || empty($customInfo)) { return $config; } if (!isset($customInfo['homeRule']) || (int)$customInfo['homeRule'] !== 1) { return $config; } $config['status'] = isset($customInfo['home']) ? (int)$customInfo['home'] : ($config['status'] ?? 1); $config['minAmount'] = $customInfo['homeAmount'] ?? ($config['minAmount'] ?? 0); $config['minNum'] = $customInfo['homeNum'] ?? ($config['minNum'] ?? 0); $config['unMeet'] = $customInfo['homeUnMeet'] ?? ($config['unMeet'] ?? 0); $config['unMeetFee'] = $customInfo['homeUnFee'] ?? ($config['unMeetFee'] ?? 0); return $config; } /** * 一次性获取商户全部配送方式配置(含 xhPsExplain / xhPsReduceRule) * @param int $shopId 门店ID * @param int $mainId 商户ID * @return array */ public static function getAllConfigs($shopId, $mainId) { $configsMap = self::getAllByCondition(['mainId' => $mainId], 'sort asc', '*', 'style', true); for ($style = 0; $style <= 4; $style++) { if (!isset($configsMap[$style])) { $defaultConfig = dict::getDict('shMethod', $style); $initData = array_merge([ 'shopId' => $shopId, 'mainId' => $mainId, 'style' => $style, ], $defaultConfig); $configObj = self::add($initData, true); $configsMap[$style] = $configObj; } } $configsArray = []; $methodIds = []; foreach ($configsMap as $style => $configObj) { $configArr = $configObj instanceof \yii\db\ActiveRecord ? $configObj->toArray() : $configObj; $configsArray[$style] = $configArr; $methodIds[] = (int)$configArr['id']; } $allExplains = []; if (!empty($methodIds)) { $allExplains = PsExplainClass::getAllByCondition(['methodId' => ['in', $methodIds]], 'sort ASC, id ASC'); } $explainsMap = []; foreach ($allExplains as $exp) { $explainsMap[(int)$exp['methodId']][] = $exp; } $allReduceRules = []; if (!empty($methodIds)) { $allReduceRules = PsReduceRuleClass::getAllByCondition(['methodId' => ['in', $methodIds]], 'sort ASC, id ASC'); } $reduceRulesMap = []; foreach ($allReduceRules as $rule) { $reduceRulesMap[(int)$rule['methodId']][] = $rule; } $result = []; for ($style = 0; $style <= 4; $style++) { $config = $configsArray[$style]; $methodId = (int)$config['id']; $config['explains'] = $explainsMap[$methodId] ?? []; $config['reduceRules'] = ($style == 2) ? ($reduceRulesMap[$methodId] ?? []) : []; $result[] = $config; } usort($result, function ($a, $b) { $sortCompare = (int)($a['sort'] ?? 0) <=> (int)($b['sort'] ?? 0); if ($sortCompare !== 0) { return $sortCompare; } return (int)($a['style'] ?? 0) <=> (int)($b['style'] ?? 0); }); return $result; } /** * 自定义计费三项是否均已有效填写(起步公里/起步价/超出每公里价均大于 0) * @param array $config * @return bool */ public static function isCustomConfigValid(array $config) { $startKm = (float)($config['startKm'] ?? 0); $startPrice = (float)($config['startPrice'] ?? 0); $perKmPrice = (float)($config['perKmPrice'] ?? 0); return $startKm > 0 && $startPrice > 0 && $perKmPrice > 0; } /** * 跑腿 style=2 是否允许启用:已绑定跑腿 或 自定义计费配置完整 * @param int $mainId 商户 ID * @param array $config 待保存的 xhPsMethod 配置 * @return bool */ public static function canEnableRunnerMethod($mainId, array $config) { if (self::isCustomConfigValid($config)) { return true; } $hasAuth = DeliveryAuthTokenClass::getByCondition(['mainId' => (int)$mainId], true); return !empty($hasAuth); } /** * 按自定义规则计距算运费(公里) * 超出起步部分:不足 1 公里按 1 公里(向上取整) * @param array $config xhPsMethod * @param float $distanceKm 配送距离(公里) * @return float * @throws \Exception */ public static function calcCustomFreightByDistance(array $config, $distanceKm) { $startKm = (float)($config['startKm'] ?? 0); $startPrice = (float)($config['startPrice'] ?? 0); $perKmPrice = (float)($config['perKmPrice'] ?? 0); $changeRate = (float)($config['changeRate'] ?? 0); if ($startKm <= 0 || $startPrice <= 0 || $perKmPrice <= 0) { throw new \Exception('自定义计费规则未配置完整'); } // 与前端一致保留两位公里 $distanceKm = round((float)$distanceKm, 2); if ($distanceKm <= 0) { throw new \Exception('算运费失败,距离无效'); } $sendCost = $startPrice; if ($distanceKm > $startKm) { $extraDistance = (float)bcsub((string)$distanceKm, (string)$startKm, 2); // 不足 1 公里按 1 公里 $billableExtra = (int)ceil($extraDistance - 1e-9); if ($billableExtra < 0) { $billableExtra = 0; } $extraCost = bcmul((string)$billableExtra, (string)$perKmPrice, 2); $sendCost = bcadd((string)$sendCost, $extraCost, 2); } if ($changeRate != 0) { $rate = bcadd('1', bcdiv((string)$changeRate, '100', 4), 4); $sendCost = bcmul((string)$sendCost, $rate, 2); } return (float)$sendCost; } /** * 花束包运费:hsFreeKm 内免费,超出按 hsPerKmPrice 每公里加收(不足 1 公里按 1 公里) * @param array $config xhPsMethod * @param float $distanceMeters 距离(米) * @return float */ public static function calcBouquetFreightByDistance(array $config, $distanceMeters) { $hsFreeKm = (float)($config['hsFreeKm'] ?? 0); $hsPerKmPrice = (float)($config['hsPerKmPrice'] ?? 0); $freeMeters = $hsFreeKm * 1000; $distanceMeters = (float)$distanceMeters; if ($distanceMeters <= $freeMeters) { return 0.0; } $diff = $distanceMeters - $freeMeters; $km = (int)ceil($diff / 1000); if ($km <= 0 || $hsPerKmPrice <= 0) { return 0.0; } return (float)bcmul((string)$km, (string)$hsPerKmPrice, 2); } /** * 纯花材是否命中 xhPsReduceRule 免跑腿 * @param array $config 含 reduceRules * @param float $distanceMeters * @param float $bigNum 花材扎数 * @param float $itemTotalAmount 商品金额 * @return bool */ public static function matchReduceRuleFree(array $config, $distanceMeters, $bigNum, $itemTotalAmount) { $rules = $config['reduceRules'] ?? []; if (!is_array($rules) || empty($rules)) { return false; } $distanceMeters = (float)$distanceMeters; $bigNum = (float)$bigNum; $itemTotalAmount = (float)$itemTotalAmount; foreach ($rules as $rule) { $meetNum = (float)($rule['meetNum'] ?? 0); $meetAmount = (float)($rule['meetAmount'] ?? 0); $freeKm = (float)($rule['freeKm'] ?? 0); if ($freeKm <= 0) { continue; } $freeMeters = $freeKm * 1000; $numOk = $meetNum <= 0 || $bigNum >= $meetNum; $amountOk = $meetAmount <= 0 || $itemTotalAmount >= $meetAmount; if ($numOk && $amountOk && $distanceMeters <= $freeMeters) { return true; } } return false; } /** * 混合单跑腿运费模式:bouquetIncluded / bouquetNotIncluded / flowerOnly * @param array $productList [['property'=>0|1,'freightType'=>0|1], ...] * @return string */ public static function resolveMixRunnerFreightMode(array $productList) { $hasBouquet = false; $hasIncluded = false; foreach ($productList as $row) { if ((int)($row['property'] ?? 1) !== 0) { continue; } $hasBouquet = true; if ((int)($row['freightType'] ?? 0) === 1) { $hasIncluded = true; } } if ($hasBouquet && $hasIncluded) { return 'bouquetIncluded'; } if ($hasBouquet) { return 'bouquetNotIncluded'; } return 'flowerOnly'; } /** * 跑腿保存/启用前校验,不通过时抛异常 * @param int $mainId 商户 ID * @param array $data 前端提交的配置 * @throws \Exception */ public static function validateRunnerSave($mainId, array $data) { $style = (int)($data['style'] ?? 0); if ($style !== 2) { return; } $status = (int)($data['status'] ?? 1); // 无论 calcType=0 用跑腿还是 =1 用自定义,都必须填写自定义计费(跑腿异常时兜底) if (!self::isCustomConfigValid($data)) { throw new \Exception('请完整填写自定义计费规则(起步公里、起步价、超出每公里价)'); } if ($status === 1 && !self::canEnableRunnerMethod($mainId, $data)) { throw new \Exception('请先绑定跑腿商户或配置完整的自定义计费后再启用'); } } /** * 取消全部跑腿授权后:若自定义计费也不完整则禁用 style=2; * 自定义完整则保持启用(calcType=1 主路径,或 calcType=0 跑腿异常时兜底) * @param int $mainId 商户 ID */ public static function disableRunnerIfNoAuthLeft($mainId) { $remaining = DeliveryAuthTokenClass::getCount(['mainId' => (int)$mainId]); if ($remaining > 0) { return; } $ps = self::getByCondition(['mainId' => (int)$mainId, 'style' => 2]); if (empty($ps)) { return; } $psArr = is_object($ps) ? $ps->toArray() : $ps; // 仍有完整自定义配置时不禁用,结算可走自定义或兜底 if (self::isCustomConfigValid($psArr)) { return; } self::updateByCondition(['id' => (int)$psArr['id']], ['status' => 0]); } /** * 保存指定配送方式配置及关联说明项、满减规则 * @param int $shopId 门店ID * @param int $mainId 商户ID * @param array $data 配置数据 * @return bool * @throws \Exception */ public static function saveConfig($shopId, $mainId, $data) { $id = $data['id'] ?? 0; $style = $data['style'] ?? 0; self::validateRunnerSave($mainId, $data); $config = null; if ($id > 0) { $config = self::getByCondition(['id' => $id, 'mainId' => $mainId], true); } if (empty($config)) { $config = self::getByCondition(['mainId' => $mainId, 'style' => $style], true); } $saveData = [ 'name' => $data['name'] ?? '', 'status' => isset($data['status']) ? (int)$data['status'] : 1, 'sort' => isset($data['sort']) ? (int)$data['sort'] : 0, 'minAmount' => isset($data['minAmount']) ? (float)$data['minAmount'] : 0.00, 'minNum' => isset($data['minNum']) ? (int)$data['minNum'] : 0, 'unMeet' => isset($data['unMeet']) ? (int)$data['unMeet'] : 0, 'unMeetFee' => isset($data['unMeetFee']) ? (float)$data['unMeetFee'] : 0.00, 'calcType' => isset($data['calcType']) ? (int)$data['calcType'] : 0, 'startKm' => isset($data['startKm']) ? (float)$data['startKm'] : 0.00, 'startPrice' => isset($data['startPrice']) ? (float)$data['startPrice'] : 0.00, 'perKmPrice' => isset($data['perKmPrice']) ? (float)$data['perKmPrice'] : 0.00, 'changeRate' => isset($data['changeRate']) ? (float)$data['changeRate'] : 0.00, 'hsFreeKm' => isset($data['hsFreeKm']) ? (float)$data['hsFreeKm'] : 0.00, 'hsPerKmPrice' => isset($data['hsPerKmPrice']) ? (float)$data['hsPerKmPrice'] : 0.00, ]; $transaction = Yii::$app->db->beginTransaction(); try { if (empty($config)) { $saveData['shopId'] = $shopId; $saveData['mainId'] = $mainId; $saveData['style'] = $style; $defaultConfig = dict::getDict('shMethod', $style); if (!empty($defaultConfig['originName'])) { $saveData['originName'] = $defaultConfig['originName']; } $config = self::add($saveData, true); } else { self::updateByCondition(['id' => $config->id], $saveData); } $methodId = $config->id; // 1. 保存说明项 xhPsExplain PsExplainClass::deleteByCondition(['methodId' => $methodId]); if (!empty($data['explains']) && is_array($data['explains'])) { $explainRows = []; foreach ($data['explains'] as $index => $exp) { if (empty($exp['explain'])) { continue; } $explainRows[] = [ 'methodId' => $methodId, 'explain' => $exp['explain'], 'color' => isset($exp['color']) ? (int)$exp['color'] : 1, 'fontWeight' => isset($exp['fontWeight']) ? (int)$exp['fontWeight'] : 1, 'sort' => isset($exp['sort']) ? (int)$exp['sort'] : $index, ]; } if (!empty($explainRows)) { PsExplainClass::batchAdd($explainRows); } } // 2. 保存跑腿满减规则 xhPsReduceRule PsReduceRuleClass::deleteByCondition(['methodId' => $methodId]); if ($style == 2 && !empty($data['reduceRules']) && is_array($data['reduceRules'])) { $ruleRows = []; foreach ($data['reduceRules'] as $index => $rule) { $ruleRows[] = [ 'methodId' => $methodId, 'meetNum' => isset($rule['meetNum']) ? (int)$rule['meetNum'] : 0, 'meetAmount' => isset($rule['meetAmount']) ? (float)$rule['meetAmount'] : 0.00, 'freeKm' => isset($rule['freeKm']) ? (float)$rule['freeKm'] : 0.00, 'sort' => isset($rule['sort']) ? (int)$rule['sort'] : $index, ]; } if (!empty($ruleRows)) { PsReduceRuleClass::batchAdd($ruleRows); } } $transaction->commit(); return true; } catch (\Exception $e) { $transaction->rollBack(); throw $e; } } /** * 合并客户送货上门单独规则(style=0 且 homeRule=1) * @param array $config xhPsMethod 配置 * @param int $style 配送方式 * @param array $customInfo 客户+花店关系信息 * @return array */ protected static function mergeCustomHomeRule($config, $style, $customInfo) { if ((int)$style === 0 && !empty($customInfo)) { return self::applyCustomHomeRule($config, $customInfo); } return $config; } /** * 判断当前订单是否未达最低消费 * 规则:满 minAmount 或 minNum 任一达标即视为达标;两者均未达标才返回 true */ public static function isBelowMinimum($itemTotalAmount, $bigNum, $config) { $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00; $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0; if ($minAmount <= 0 && $minNum <= 0) { return false; } $amountOk = $minAmount <= 0 || (float)$itemTotalAmount >= $minAmount; $numOk = $minNum <= 0 || (float)$bigNum >= $minNum; return !($amountOk || $numOk); } /** * 拼接最低消费门槛文案,如 200元或30扎 */ protected static function buildUnMeetConditionText($config) { $parts = []; $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00; $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0; if ($minAmount > 0) { $parts[] = $minAmount . '元'; } if ($minNum > 0) { $parts[] = $minNum . '扎'; } return implode('或', $parts); } /** * 未达门槛时「还差多少可免」提示,如:订单还差20元和3扎可免包装费 * @param array $config * @param float $itemTotalAmount * @param float $bigNum * @param string $feeLabel 包装费|运费 * @return string */ protected static function buildUnMeetShortTip($config, $itemTotalAmount, $bigNum, $feeLabel) { $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00; $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0; $parts = []; if ($minAmount > 0) { $shortAmount = round(max(0, $minAmount - (float)$itemTotalAmount), 2); if ($shortAmount > 0) { $parts[] = $shortAmount . '元'; } } if ($minNum > 0) { $shortNum = max(0, $minNum - (float)$bigNum); // 扎数展示取整友好值 if ($shortNum > 0) { $parts[] = (fmod($shortNum, 1) == 0 ? (int)$shortNum : round($shortNum, 2)) . '扎'; } } if (empty($parts)) { return ''; } return '订单还差' . implode('和', $parts) . '可免' . $feeLabel; } /** * 判断当天是否已收过包装类附加费(xhPsMethod unMeet=2) */ public static function isPackPaidToday($customId, $sendType = 0) { if (empty($customId)) { return false; } $current = date('Y_m_d'); $key = 'hdCustom_hasGetPackCost:' . (int)$sendType . '_' . $current . ':' . $customId; $has = Yii::$app->redis->executeCommand('GET', [$key]); return (!empty($has) && $has == 1); } /** * 当天首单未收包装费时标记(后续单不再收) */ public static function isPackFeeSkippedToday($customId, $sendType = 0) { if (empty($customId)) { return false; } $current = date('Y_m_d'); $key = 'hdCustom_skipPackCost:' . (int)$sendType . '_' . $current . ':' . $customId; $has = Yii::$app->redis->executeCommand('GET', [$key]); return (!empty($has) && $has == 1); } /** * 当天是否已对包装附加费做过决策(已收或首单免收) */ public static function hasPackFeeDecisionToday($customId, $sendType = 0) { return self::isPackPaidToday($customId, $sendType) || self::isPackFeeSkippedToday($customId, $sendType); } /** * 判断当天是否已收过运费类附加费(xhPsMethod unMeet=0) */ public static function isSendCostPaidToday($customId, $sendType = 0) { if (empty($customId)) { return false; } $current = date('Y_m_d'); $key = 'hdCustom_hasGetSendCost:' . (int)$sendType . '_' . $current . ':' . $customId; $has = Yii::$app->redis->executeCommand('GET', [$key]); return (!empty($has) && $has == 1); } /** * 当天首单未收附加运费时标记(后续单不再收) */ public static function isSendFeeSkippedToday($customId, $sendType = 0) { if (empty($customId)) { return false; } $current = date('Y_m_d'); $key = 'hdCustom_skipSendCost:' . (int)$sendType . '_' . $current . ':' . $customId; $has = Yii::$app->redis->executeCommand('GET', [$key]); return (!empty($has) && $has == 1); } /** * 当天是否已对附加运费做过决策(已收或首单免收) */ public static function hasSendFeeDecisionToday($customId, $sendType = 0) { return self::isSendCostPaidToday($customId, $sendType) || self::isSendFeeSkippedToday($customId, $sendType); } /** * 支付成功后标记当天已收附加费,保证同一客户同配送方式每日只收一次 */ public static function markUnMeetFeePaid($customId, $sendType, $packingFee = 0, $unMeetSendCost = 0) { self::recordDailyUnMeetFeeState($customId, $sendType, $packingFee, $unMeetSendCost); } /** * 首单支付后记录当日附加费决策:收过则不再收;首单未收则当日后续也不收 */ protected static function recordDailyUnMeetFeeState($customId, $sendType, $packingFee = 0, $unMeetSendCost = 0) { if (empty($customId)) { return; } $current = date('Y_m_d'); $style = (int)$sendType; $ttl = 86400; if (!self::hasSendFeeDecisionToday($customId, $style)) { if ((float)$unMeetSendCost > 0) { $sendKey = 'hdCustom_hasGetSendCost:' . $style . '_' . $current . ':' . $customId; Yii::$app->redis->executeCommand('SETEX', [$sendKey, $ttl, '1']); } else { $skipKey = 'hdCustom_skipSendCost:' . $style . '_' . $current . ':' . $customId; Yii::$app->redis->executeCommand('SETEX', [$skipKey, $ttl, '1']); } } if (!self::hasPackFeeDecisionToday($customId, $style)) { if ((float)$packingFee > 0) { $packKey = 'hdCustom_hasGetPackCost:' . $style . '_' . $current . ':' . $customId; Yii::$app->redis->executeCommand('SETEX', [$packKey, $ttl, '1']); } else { $skipKey = 'hdCustom_skipPackCost:' . $style . '_' . $current . ':' . $customId; Yii::$app->redis->executeCommand('SETEX', [$skipKey, $ttl, '1']); } } } /** * 根据订单信息标记当日附加费(支付成功回调/余额支付) */ public static function markUnMeetFeePaidFromOrder($order) { if (empty($order)) { return; } $orderSn = is_object($order) ? ($order->orderSn ?? '') : ($order['orderSn'] ?? ''); $customId = (int)(is_object($order) ? ($order->customId ?? 0) : ($order['customId'] ?? 0)); $sendType = (int)(is_object($order) ? ($order->sendType ?? 0) : ($order['sendType'] ?? 0)); $packingFee = (float)(is_object($order) ? ($order->packingFee ?? 0) : ($order['packingFee'] ?? 0)); $unMeetSendCost = 0; if (!empty($orderSn)) { $unMeetSendKey = 'hd_order_unmeet_send:' . $orderSn; $cached = Yii::$app->redis->executeCommand('GET', [$unMeetSendKey]); if (!empty($cached)) { $unMeetSendCost = (float)$cached; } } // 无附加包装费/附加运费时不写入(避免满足门槛的订单误记 skip) if ($packingFee <= 0 && $unMeetSendCost <= 0) { return; } self::recordDailyUnMeetFeeState($customId, $sendType, $packingFee, $unMeetSendCost); } /** * 合并 hd + custom 为 calcFee / applyCustomHomeRule 使用的客户信息(与商城 getAllMethod 一致) * @param mixed $hd 花店关系 xhHd * @param mixed $custom 客户 xhCustom * @return array */ public static function buildCustomInfo($hd = null, $custom = null) { $customInfo = []; if (!empty($hd)) { if (is_array($hd)) { $customInfo = $hd; } elseif (is_object($hd) && method_exists($hd, 'toArray')) { $customInfo = $hd->toArray(); } elseif (is_object($hd) && isset($hd->attributes)) { $customInfo = $hd->attributes; } else { $customInfo = (array)$hd; } } if (!empty($custom)) { if (is_array($custom)) { $customArr = $custom; } elseif (is_object($custom) && method_exists($custom, 'toArray')) { $customArr = $custom->toArray(); } elseif (is_object($custom) && isset($custom->attributes)) { $customArr = $custom->attributes; } else { $customArr = (array)$custom; } $customInfo = array_merge($customInfo, $customArr); } return $customInfo; } /** * 对配送方式列表应用 style=0 客户单独送货上门规则 * @param array $configs getAllConfigs 结果 * @param array $customInfo buildCustomInfo 结果 * @return array */ public static function applyCustomHomeRuleToConfigs(array $configs, array $customInfo) { if (empty($customInfo)) { return $configs; } foreach ($configs as &$config) { if ((int)($config['style'] ?? 0) === 0) { $config = self::applyCustomHomeRule($config, $customInfo); } } unset($config); return $configs; } /** * 标记当日附加费是否已决策(供商城 getAllMethod 下发锁定态) * @param array $configs 配送方式列表 * @param int $customId 花店客户 id * @return array */ public static function applyDailyFeeLockFlags(array $configs, $customId) { $customId = (int)$customId; if ($customId <= 0) { return $configs; } foreach ($configs as &$config) { $style = (int)($config['style'] ?? 0); $unMeet = (int)($config['unMeet'] ?? 0); $hasSendDecision = self::hasSendFeeDecisionToday($customId, $style); $hasPackDecision = self::hasPackFeeDecisionToday($customId, $style); $config['todaySendFeeLocked'] = $hasSendDecision ? 1 : 0; $config['todayPackFeeLocked'] = $hasPackDecision ? 1 : 0; if ($unMeet === 0 && $hasSendDecision) { $config['minAmount'] = 0; $config['minNum'] = 0; $config['unMeetFee'] = 0; } elseif ($unMeet === 2 && $hasPackDecision) { $config['minAmount'] = 0; $config['minNum'] = 0; $config['unMeetFee'] = 0; } } unset($config); return $configs; } /** * 商城混合结算:统一入口调用 calcFee(预览与下单共用) */ public static function calcFeeForMallContext($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $customInfo = []) { return self::calcFee($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId, is_array($customInfo) ? $customInfo : []); } /** * 商城结算页预览未达门槛附加运费/包装费(与 create-mix-order 内 calcFee 同源) * @return array */ public static function previewMixUnMeetFee($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $customInfo = []) { $costs = self::calcFeeForMallContext($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId, $customInfo); $style = (int)$sendType; $config = self::getConfig($shopId, $mainId, $style); if (empty($config)) { return [ 'sendCost' => 0, 'packCost' => 0, 'unMeetSendCost' => 0, 'packingFee' => 0, 'belowMinimum' => false, 'canOrder' => true, 'tip' => '', ]; } $config = self::mergeCustomHomeRule($config, $style, $customInfo); $belowMinimum = self::isBelowMinimum($itemTotalAmount, $bigNum, $config); $tip = ''; $canOrder = true; if ($belowMinimum) { $unMeet = (int)($config['unMeet'] ?? 0); $unMeetFee = (float)($config['unMeetFee'] ?? 0); $conditionText = self::buildUnMeetConditionText($config); if ($unMeet === 1) { $canOrder = false; $tip = $conditionText !== '' ? "订单不满{$conditionText},不能下单" : '未达到最低消费,不能下单'; } elseif (($unMeet === 0 || $unMeet === 2) && $unMeetFee > 0) { $locked = ($unMeet === 0 && self::hasSendFeeDecisionToday($customId, $style)) || ($unMeet === 2 && self::hasPackFeeDecisionToday($customId, $style)); if (!$locked) { $feeLabel = $unMeet === 2 ? '包装费' : '运费'; // 展示还差多少可免附加费,便于用户凑单 $tip = self::buildUnMeetShortTip($config, $itemTotalAmount, $bigNum, $feeLabel); } } } $checkErr = self::checkLimit( $shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $costs['packCost'], $costs['sendCost'], $customId, $customInfo ); if ($checkErr !== '') { $canOrder = false; $tip = $checkErr; } return [ 'sendCost' => (float)$costs['sendCost'], 'packCost' => (float)$costs['packCost'], 'unMeetSendCost' => (float)$costs['sendCost'], 'packingFee' => (float)$costs['packCost'], 'belowMinimum' => $belowMinimum, 'canOrder' => $canOrder, 'tip' => $tip, ]; } /** * 混合结算页:一次预览各购买方式未达门槛附加费(不含 style=2 跑腿距离运费) * @return array 键为 style 字符串,如 ['0' => [...], '1' => [...]] */ public static function previewMixUnMeetFeeBatch($shopId, $mainId, $itemTotalAmount, $bigNum, $customId = 0, $hd = null, $custom = null) { $customInfo = self::buildCustomInfo($hd, $custom); $fees = []; for ($style = 0; $style <= 4; $style++) { $fees[(string)$style] = self::previewMixUnMeetFee( $shopId, $mainId, $style, $itemTotalAmount, $bigNum, (int)$customId, $customInfo ); } return $fees; } /** * 计算未达最低消费时应写入订单的附加运费/包装费 * @return array ['sendCost' => 附加运费, 'packCost' => 附加包装费] */ public static function calcFee($shopId, $mainId, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $customInfo = []) { $costs = ['sendCost' => 0, 'packCost' => 0]; $style = (int)$sendType; if ($style < 0 || $style > 4) { return $costs; } $config = self::getConfig($shopId, $mainId, $style); if (empty($config)) { return $costs; } $config = self::mergeCustomHomeRule($config, $style, $customInfo); if (!self::isBelowMinimum($itemTotalAmount, $bigNum, $config)) { return $costs; } $unMeet = isset($config['unMeet']) ? (int)$config['unMeet'] : 0; $unMeetFee = isset($config['unMeetFee']) ? (float)$config['unMeetFee'] : 0; if ($unMeetFee <= 0) { return $costs; } // unMeet: 0 加收运费,2 加收包装费(每客户每购买方式每日至多收一次;首单未收则当日不再收) if ($unMeet === 0 && !self::hasSendFeeDecisionToday($customId, $style)) { $costs['sendCost'] = $unMeetFee; } elseif ($unMeet === 2 && !self::hasPackFeeDecisionToday($customId, $style)) { $costs['packCost'] = $unMeetFee; } return $costs; } /** * 下单前校验配送方式最低消费与附加费是否已正确写入 * @param float $packCost 附加包装费 * @param float $unMeetSendCost 未达门槛附加运费(不含跑腿报价) * @return string 空字符串表示通过 */ public static function checkLimit($shopId, $mainId, $sendType, $actPrice, $bigNum, $packCost = 0, $unMeetSendCost = 0, $customId = 0, $customInfo = []) { $style = (int)$sendType; if ($style < 0 || $style > 4) { return '配送方式无效'; } $config = self::getConfig($shopId, $mainId, $style); if (empty($config)) { return '配送方式配置异常'; } $config = self::mergeCustomHomeRule($config, $style, $customInfo); $methodName = !empty($config['name']) ? $config['name'] : '该配送方式'; if (isset($config['status']) && (int)$config['status'] === 0) { return "暂不支持{$methodName},请选其它配送方式"; } if (!self::isBelowMinimum($actPrice, $bigNum, $config)) { return ''; } $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00; $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0; $unMeet = isset($config['unMeet']) ? (int)$config['unMeet'] : 0; $unMeetFee = isset($config['unMeetFee']) ? (float)$config['unMeetFee'] : 0; if ($unMeet === 1) { $conditionText = self::buildUnMeetConditionText($config); if ($conditionText !== '') { return "订单不满{$conditionText},不能下单"; } return '未达到最低消费,不能下单'; } if (($unMeet === 0 || $unMeet === 2) && $unMeetFee > 0) { $feeToCheck = $unMeet === 2 ? $packCost : $unMeetSendCost; if ($unMeet === 2 && self::hasPackFeeDecisionToday($customId, $style)) { return ''; } if ($unMeet === 0 && self::hasSendFeeDecisionToday($customId, $style)) { return ''; } if (bccomp((string)$feeToCheck, (string)$unMeetFee, 2) < 0) { $feeLabel = $unMeet === 2 ? '包装费' : '运费'; $conditionText = self::buildUnMeetConditionText($config); if ($conditionText !== '') { return "订单不满{$conditionText},加{$feeLabel}{$unMeetFee}元"; } return "未达最低消费,需加收{$feeLabel}{$unMeetFee}元"; } } return ''; } }