| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382 |
- <?php
- /**
- * 用途:花店配送方式配置业务逻辑类
- * 谁用:花店系统 hdApp 配送方式设置页、商城混合结算附加费预览/下单
- * 解决什么问题:读写 xhPsMethod / xhPsExplain / xhPsReduceRule;附加费支持按自然日或冷却秒重收
- */
- namespace bizHd\order\classes;
- use bizGhs\express\classes\DeliveryAuthTokenClass;
- use bizHd\base\classes\BaseClass;
- use common\components\dict;
- use Yii;
- class PsMethodClass extends BaseClass
- {
- public static $baseFile = '\bizHd\order\models\PsMethod';
- /**
- * 获取所有配送方式的排序和别名信息
- * @param int $mainId 商户ID
- * @return array
- */
- public static function getSorts($mainId)
- {
- $list = [];
- $aliases = [
- 0 => '送货',
- 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,
- // 附加费重收:0 自然日;1 冷却秒(配置页按小时录入后换算)
- 'unMeetLockType' => (isset($data['unMeetLockType']) && (int)$data['unMeetLockType'] === 1) ? 1 : 0,
- 'unMeetLockSeconds' => self::normalizeUnMeetLockSeconds($data['unMeetLockSeconds'] ?? 1800),
- '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;
- }
- /**
- * 判断当前订单是否未达最低消费
- * 业务规则:已配置的最低金额、最低数量须同时满足才算达标;任一未达(填 0 表示不限)即收附加费/限制下单
- * 供 mall affirmMix 预览、calcFee、checkLimit 共用
- */
- 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;
- }
- /**
- * 规范化冷却秒数(至少 1 秒;非法则回退默认 1800)
- * @param mixed $seconds
- * @return int
- */
- public static function normalizeUnMeetLockSeconds($seconds)
- {
- $n = (int)$seconds;
- if ($n <= 0) {
- return 1800;
- }
- return $n;
- }
- /**
- * 从 xhPsMethod 配置解析附加费重收策略
- * @param array|null $config
- * @return array ['type'=>0|1, 'seconds'=>int] type:0自然日 1冷却秒
- */
- public static function resolveUnMeetLockOptions($config = null)
- {
- $type = 0;
- $seconds = 1800;
- if (is_array($config)) {
- $type = (int)($config['unMeetLockType'] ?? 0);
- $seconds = self::normalizeUnMeetLockSeconds($config['unMeetLockSeconds'] ?? 1800);
- }
- if ($type !== 1) {
- $type = 0;
- }
- return ['type' => $type, 'seconds' => $seconds];
- }
- /**
- * 按配置判断运费类附加费是否仍在锁定期
- * - 间隔模式(type=1):只按「收款时间 + 当前配置秒数」,到期即再收;不看自然日/今日订单
- * - 自然日(type=0):当日 Redis 决策 + 间隔痕迹互认 + 今日已付订单兜底
- * @param int $customId
- * @param int $sendType
- * @param array|null $config xhPsMethod 行;缺省按自然日
- * @return bool
- */
- public static function isUnMeetSendFeeLocked($customId, $sendType = 0, $config = null)
- {
- $customId = (int)$customId;
- if ($customId <= 0) {
- // 无客户身份无法记锁,每次都会算附加费(与 BaseController 未绑定 hd.customId 时一致)
- return false;
- }
- $lock = self::resolveUnMeetLockOptions($config);
- // 间隔:严格按时长,改成 4 秒后过期即应再收,不能被「今日已收」挡住
- if ($lock['type'] === 1) {
- return self::isSendCostInCooldown($customId, $sendType, $lock['seconds']);
- }
- if (self::hasSendFeeDecisionToday($customId, $sendType)) {
- return true;
- }
- if (self::hasIntervalSendFeeCollectedToday($customId, $sendType)) {
- // 间隔痕迹还在:补写自然日 key,避免后续再丢
- self::ensureDaySendFeePaidMarked($customId, $sendType);
- return true;
- }
- return self::hasPaidUnMeetFeeOrderToday($customId, $sendType, 0);
- }
- /**
- * 按配置判断包装类附加费是否仍在锁定期
- * @param int $customId
- * @param int $sendType
- * @param array|null $config
- * @return bool
- */
- public static function isUnMeetPackFeeLocked($customId, $sendType = 0, $config = null)
- {
- $customId = (int)$customId;
- if ($customId <= 0) {
- return false;
- }
- $lock = self::resolveUnMeetLockOptions($config);
- if ($lock['type'] === 1) {
- return self::isPackCostInCooldown($customId, $sendType, $lock['seconds']);
- }
- if (self::hasPackFeeDecisionToday($customId, $sendType)) {
- return true;
- }
- if (self::hasIntervalPackFeeCollectedToday($customId, $sendType)) {
- self::ensureDayPackFeePaidMarked($customId, $sendType);
- return true;
- }
- return self::hasPaidUnMeetFeeOrderToday($customId, $sendType, 2);
- }
- /**
- * 补写自然日「运费附加费已收」标记
- */
- protected static function ensureDaySendFeePaidMarked($customId, $sendType)
- {
- if (self::isSendCostPaidToday($customId, $sendType)) {
- return;
- }
- $key = 'hdCustom_hasGetSendCost:' . (int)$sendType . '_' . date('Y_m_d') . ':' . (int)$customId;
- Yii::$app->redis->executeCommand('SETEX', [$key, 86400, '1']);
- }
- /**
- * 补写自然日「包装附加费已收」标记
- */
- protected static function ensureDayPackFeePaidMarked($customId, $sendType)
- {
- if (self::isPackPaidToday($customId, $sendType)) {
- return;
- }
- $key = 'hdCustom_hasGetPackCost:' . (int)$sendType . '_' . date('Y_m_d') . ':' . (int)$customId;
- Yii::$app->redis->executeCommand('SETEX', [$key, 86400, '1']);
- }
- /**
- * Redis 无锁时:回查今日已支付订单是否已收过对应附加费(修复短间隔逻辑过期删 key / 切换模式丢痕迹)
- * @param int $customId
- * @param int $sendType
- * @param int $unMeetKind 0=运费附加费(送货/物流/快递看 sendCost) 2=包装费(packingFee)
- * @return bool
- */
- protected static function hasPaidUnMeetFeeOrderToday($customId, $sendType, $unMeetKind = 0)
- {
- $customId = (int)$customId;
- $sendType = (int)$sendType;
- if ($customId <= 0) {
- return false;
- }
- $start = date('Y-m-d 00:00:00');
- $end = date('Y-m-d 23:59:59');
- try {
- $query = \bizHd\order\models\Order::find()
- ->where([
- 'customId' => $customId,
- 'sendType' => $sendType,
- 'payStatus' => 1,
- 'fromType' => (int)dict::getDict('fromType', 'mall'),
- ])
- ->andWhere(['>=', 'payTime', $start])
- ->andWhere(['<=', 'payTime', $end]);
- if ((int)$unMeetKind === 2) {
- $query->andWhere(['>', 'packingFee', 0]);
- } elseif (in_array($sendType, [0, 3, 4], true)) {
- // 送货/物流/快递:页内 sendCost 基本即未达门槛附加运费
- $query->andWhere([
- 'or',
- ['>', 'packingFee', 0],
- ['>', 'sendCost', 0],
- ]);
- } else {
- // 跑腿等:仅包装附加费可明确识别
- $query->andWhere(['>', 'packingFee', 0]);
- }
- $exists = $query->exists();
- if ($exists) {
- // 回写 Redis,后续 calc-mix-fee-batch 不再打库
- if ((int)$unMeetKind === 2) {
- self::ensureDayPackFeePaidMarked($customId, $sendType);
- } else {
- self::ensureDaySendFeePaidMarked($customId, $sendType);
- }
- }
- return $exists;
- } catch (\Exception $e) {
- Yii::error('hasPaidUnMeetFeeOrderToday failed: ' . $e->getMessage(), __METHOD__);
- return false;
- }
- }
- /**
- * 冷却 Redis key 最长保留时间(秒):逻辑是否锁定用「收款时间+当前配置间隔」算,
- * key 本身多留几天,避免改长间隔时 key 已被 Redis 删掉导致无法按新间隔生效
- */
- const UNMEET_COOLDOWN_REDIS_KEEP_SECONDS = 604800; // 7 天
- /**
- * 冷却模式下:运费附加费 Redis key(无日期)
- */
- protected static function buildSendCostCooldownKey($sendType, $customId)
- {
- return 'hdCustom_hasGetSendCost:interval:' . (int)$sendType . ':' . (int)$customId;
- }
- /**
- * 冷却模式下:包装附加费 Redis key
- */
- protected static function buildPackCostCooldownKey($sendType, $customId)
- {
- return 'hdCustom_hasGetPackCost:interval:' . (int)$sendType . ':' . (int)$customId;
- }
- /**
- * 间隔冷却 key 是否记录了「今天」收过运费附加费(供改成自然日后互认)
- * @param int $customId
- * @param int $sendType
- * @return bool
- */
- protected static function hasIntervalSendFeeCollectedToday($customId, $sendType)
- {
- return self::hasIntervalFeeCollectedToday(self::buildSendCostCooldownKey($sendType, $customId));
- }
- /**
- * 间隔冷却 key 是否记录了「今天」收过包装附加费
- * @param int $customId
- * @param int $sendType
- * @return bool
- */
- protected static function hasIntervalPackFeeCollectedToday($customId, $sendType)
- {
- return self::hasIntervalFeeCollectedToday(self::buildPackCostCooldownKey($sendType, $customId));
- }
- /**
- * interval key 存在且收款日为今天则视为今日已收过
- * @param string $key
- * @return bool
- */
- protected static function hasIntervalFeeCollectedToday($key)
- {
- $has = Yii::$app->redis->executeCommand('GET', [$key]);
- if ($has === null || $has === false || $has === '') {
- return false;
- }
- $val = (int)$has;
- // 新格式:收款时间戳落在当天
- if ($val > 1000000000) {
- return date('Y-m-d', $val) === date('Y-m-d');
- }
- // 旧格式:key 仍在则保守视为今日已收,避免切换自然日后立刻再收
- return true;
- }
- /**
- * 解析冷却 Redis TTL(至少覆盖当前间隔,且不少于 7 天便于改配置后仍能读到收款时间)
- * @param int $lockSeconds
- * @return int
- */
- protected static function resolveCooldownRedisTtl($lockSeconds)
- {
- return max((int)$lockSeconds, self::UNMEET_COOLDOWN_REDIS_KEEP_SECONDS, 1);
- }
- /**
- * 判断间隔冷却是否仍有效(仅用于 unMeetLockType=1)
- * value 必须是收款时间戳;按「距收款 < 当前配置秒数」判断,改间隔立即生效
- * @param string $key
- * @param int $expectedSeconds 当前配置的冷却秒数
- * @return bool
- */
- protected static function isIntervalCooldownActive($key, $expectedSeconds)
- {
- $has = Yii::$app->redis->executeCommand('GET', [$key]);
- if ($has === null || $has === false || $has === '') {
- return false;
- }
- $expected = max(1, (int)$expectedSeconds);
- $val = (int)$has;
- // 仅新格式(unix 收款时间)可按当前间隔秒判断
- if ($val > 1000000000) {
- return (time() - $val) < $expected;
- }
- // 旧格式无时间戳:不能按 4 秒/2 小时判断,避免 key 残留导致永远不收费
- return false;
- }
- /**
- * 冷却秒模式:是否仍在运费附加费冷却期内(按当前配置间隔即时计算)
- * @param int $customId
- * @param int $sendType
- * @param int|null $expectedSeconds 当前配置冷却秒数,缺省按 1800
- * @return bool
- */
- public static function isSendCostInCooldown($customId, $sendType = 0, $expectedSeconds = null)
- {
- if (empty($customId)) {
- return false;
- }
- $seconds = $expectedSeconds !== null ? (int)$expectedSeconds : 1800;
- return self::isIntervalCooldownActive(self::buildSendCostCooldownKey($sendType, $customId), $seconds);
- }
- /**
- * 冷却秒模式:是否仍在包装附加费冷却期内(按当前配置间隔即时计算)
- * @param int $customId
- * @param int $sendType
- * @param int|null $expectedSeconds
- * @return bool
- */
- public static function isPackCostInCooldown($customId, $sendType = 0, $expectedSeconds = null)
- {
- if (empty($customId)) {
- return false;
- }
- $seconds = $expectedSeconds !== null ? (int)$expectedSeconds : 1800;
- return self::isIntervalCooldownActive(self::buildPackCostCooldownKey($sendType, $customId), $seconds);
- }
- /**
- * 判断当天是否已收过包装类附加费(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);
- }
- /**
- * 支付成功后标记已收附加费(兼容旧调用;优先走 markUnMeetFeePaidFromOrder)
- * @param int $customId
- * @param int $sendType
- * @param float $packingFee
- * @param float $unMeetSendCost
- * @param array|null $config xhPsMethod,用于区分自然日/冷却秒
- */
- public static function markUnMeetFeePaid($customId, $sendType, $packingFee = 0, $unMeetSendCost = 0, $config = null)
- {
- self::recordUnMeetFeeLockState($customId, $sendType, $packingFee, $unMeetSendCost, $config);
- }
- /**
- * 写入附加费锁定态:实收时「间隔时间戳 + 自然日已收」双写,切换模式可互认
- * @param int $customId
- * @param int $sendType
- * @param float $packingFee
- * @param float $unMeetSendCost
- * @param array|null $config
- */
- protected static function recordUnMeetFeeLockState($customId, $sendType, $packingFee = 0, $unMeetSendCost = 0, $config = null)
- {
- if (empty($customId)) {
- return;
- }
- $style = (int)$sendType;
- $lock = self::resolveUnMeetLockOptions($config);
- $current = date('Y_m_d');
- $dayTtl = 86400;
- $lockedAt = (string)time();
- $intervalTtl = self::resolveCooldownRedisTtl(max(1, (int)($lock['seconds'] ?? 1800)));
- $collectedSend = (float)$unMeetSendCost > 0;
- $collectedPack = (float)$packingFee > 0;
- // 实收运费附加费:间隔 key(时间戳)+ 自然日已收 key 一并写入
- if ($collectedSend) {
- Yii::$app->redis->executeCommand('SETEX', [self::buildSendCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
- if (!self::isSendCostPaidToday($customId, $style)) {
- $sendKey = 'hdCustom_hasGetSendCost:' . $style . '_' . $current . ':' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$sendKey, $dayTtl, '1']);
- }
- } elseif ((int)$lock['type'] !== 1 && !self::hasSendFeeDecisionToday($customId, $style)) {
- // 仅自然日模式:首单未收附加运费则记 skip,当日不再收
- $skipKey = 'hdCustom_skipSendCost:' . $style . '_' . $current . ':' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$skipKey, $dayTtl, '1']);
- }
- // 实收包装附加费:同样双写
- if ($collectedPack) {
- Yii::$app->redis->executeCommand('SETEX', [self::buildPackCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
- if (!self::isPackPaidToday($customId, $style)) {
- $packKey = 'hdCustom_hasGetPackCost:' . $style . '_' . $current . ':' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$packKey, $dayTtl, '1']);
- }
- } elseif ((int)$lock['type'] !== 1 && !self::hasPackFeeDecisionToday($customId, $style)) {
- $skipKey = 'hdCustom_skipPackCost:' . $style . '_' . $current . ':' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$skipKey, $dayTtl, '1']);
- }
- }
- /**
- * 根据订单信息标记附加费锁定(支付成功回调/余额支付)
- * 会按订单 mainId+sendType 读取 xhPsMethod 的重收规则
- *
- * 注意:附加运费金额优先读 redis hd_order_unmeet_send:{orderSn};
- * 微信拉起支付可能更换 orderSn,导致 redis 读空,需用订单 sendCost + 配置回退,
- * 否则冷却/自然日锁永远写不进去,结算页会反复加收。
- */
- 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));
- $shopId = (int)(is_object($order) ? ($order->shopId ?? 0) : ($order['shopId'] ?? 0));
- $mainId = (int)(is_object($order) ? ($order->mainId ?? 0) : ($order['mainId'] ?? 0));
- $packingFee = (float)(is_object($order) ? ($order->packingFee ?? 0) : ($order['packingFee'] ?? 0));
- $orderSendCost = (float)(is_object($order) ? ($order->sendCost ?? 0) : ($order['sendCost'] ?? 0));
- $config = null;
- if ($mainId > 0) {
- // 支付回写只需 lock / unMeet 字段;无完整 explains 亦可
- $row = self::getByCondition(['mainId' => $mainId, 'style' => $sendType]);
- if (!empty($row)) {
- $config = is_object($row) && method_exists($row, 'toArray') ? $row->toArray() : (array)$row;
- } elseif ($shopId > 0) {
- $config = self::getConfig($shopId, $mainId, $sendType);
- }
- }
- $unMeetSendCost = 0;
- if (!empty($orderSn)) {
- $unMeetSendKey = 'hd_order_unmeet_send:' . $orderSn;
- $cached = Yii::$app->redis->executeCommand('GET', [$unMeetSendKey]);
- if (!empty($cached)) {
- $unMeetSendCost = (float)$cached;
- }
- }
- // redis 丢失回退:送货/物流/快递的 sendCost 基本就是未达门槛附加运费(无跑腿距离费)
- if ($unMeetSendCost <= 0 && !empty($config) && in_array($sendType, [0, 3, 4], true)) {
- $unMeet = (int)($config['unMeet'] ?? 0);
- $unMeetFee = (float)($config['unMeetFee'] ?? 0);
- if ($unMeet === 0 && $unMeetFee > 0 && $orderSendCost > 0) {
- $unMeetSendCost = $unMeetFee;
- }
- }
- // 无附加包装费/附加运费时不写入(避免满足门槛的订单误记 skip)
- if ($packingFee <= 0 && $unMeetSendCost <= 0) {
- return;
- }
- self::recordUnMeetFeeLockState($customId, $sendType, $packingFee, $unMeetSendCost, $config);
- }
- /**
- * 微信支付换单号时迁移附加运费缓存,避免支付成功读不到金额导致不写冷却锁
- * @param string $oldOrderSn
- * @param string $newOrderSn
- */
- public static function migrateUnMeetSendCostCache($oldOrderSn, $newOrderSn)
- {
- $oldOrderSn = (string)$oldOrderSn;
- $newOrderSn = (string)$newOrderSn;
- if ($oldOrderSn === '' || $newOrderSn === '' || $oldOrderSn === $newOrderSn) {
- return;
- }
- $oldKey = 'hd_order_unmeet_send:' . $oldOrderSn;
- $cached = Yii::$app->redis->executeCommand('GET', [$oldKey]);
- if ($cached === null || $cached === false || $cached === '') {
- return;
- }
- $ttl = Yii::$app->redis->executeCommand('TTL', [$oldKey]);
- $ttl = (int)$ttl;
- if ($ttl <= 0) {
- $ttl = 864000;
- }
- $newKey = 'hd_order_unmeet_send:' . $newOrderSn;
- Yii::$app->redis->executeCommand('SETEX', [$newKey, $ttl, (string)$cached]);
- }
- /**
- * 合并 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::isUnMeetSendFeeLocked($customId, $style, $config);
- $hasPackDecision = self::isUnMeetPackFeeLocked($customId, $style, $config);
- $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::isUnMeetSendFeeLocked($customId, $style, $config))
- || ($unMeet === 2 && self::isUnMeetPackFeeLocked($customId, $style, $config));
- 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 加收包装费;锁定策略见 unMeetLockType(自然日或冷却秒)
- if ($unMeet === 0 && !self::isUnMeetSendFeeLocked($customId, $style, $config)) {
- $costs['sendCost'] = $unMeetFee;
- } elseif ($unMeet === 2 && !self::isUnMeetPackFeeLocked($customId, $style, $config)) {
- $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::isUnMeetPackFeeLocked($customId, $style, $config)) {
- return '';
- }
- if ($unMeet === 0 && self::isUnMeetSendFeeLocked($customId, $style, $config)) {
- 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 '';
- }
- }
|