| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000 |
- <?php
- /**
- * 用途:花店配送方式配置业务逻辑类
- * 谁用:花店系统 hdApp 配送方式设置页
- * 解决什么问题:读写 xhPsMethod / xhPsExplain / xhPsReduceRule,与供货商 xhSh* 分离
- */
- 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,
- '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 '';
- }
- }
|