| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- <?php
- /**
- * 用途:花店配送方式配置业务逻辑类
- * 谁用:花店系统 hdApp 配送方式设置页
- * 解决什么问题:读写 xhPsMethod / xhPsExplain / xhPsReduceRule,与供货商 xhSh* 分离
- */
- namespace bizHd\order\classes;
- 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;
- }
- /**
- * 保存指定配送方式配置及关联说明项、满减规则
- * @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;
- $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,
- '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,
- ];
- $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);
- }
- /**
- * 判断当天是否已收过包装类附加费(xhPsMethod unMeet=2)
- */
- public static function isPackPaidToday($customId, $sendType = 0)
- {
- if (empty($customId)) {
- return false;
- }
- $current = date('Y_m_d');
- $key = 'hd_ps_custom_today_has_get_pack_cost_' . (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 = 'hd_ps_custom_today_skip_pack_cost_' . (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 = 'hd_ps_custom_today_has_get_send_cost_' . (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 = 'hd_ps_custom_today_skip_send_cost_' . (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 = 'hd_ps_custom_today_has_get_send_cost_' . $style . '_' . $current . '_' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$sendKey, $ttl, '1']);
- } else {
- $skipKey = 'hd_ps_custom_today_skip_send_cost_' . $style . '_' . $current . '_' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$skipKey, $ttl, '1']);
- }
- }
- if (!self::hasPackFeeDecisionToday($customId, $style)) {
- if ((float)$packingFee > 0) {
- $packKey = 'hd_ps_custom_today_has_get_pack_cost_' . $style . '_' . $current . '_' . $customId;
- Yii::$app->redis->executeCommand('SETEX', [$packKey, $ttl, '1']);
- } else {
- $skipKey = 'hd_ps_custom_today_skip_pack_cost_' . $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_ps_order_unmeet_send_' . $orderSn;
- $cached = Yii::$app->redis->executeCommand('GET', [$unMeetSendKey]);
- if (!empty($cached)) {
- $unMeetSendCost = (float)$cached;
- }
- }
- self::recordDailyUnMeetFeeState($customId, $sendType, $packingFee, $unMeetSendCost);
- }
- /**
- * 计算未达最低消费时应写入订单的附加运费/包装费
- * @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 '';
- }
- }
|