|
|
@@ -1,8 +1,8 @@
|
|
|
<?php
|
|
|
/**
|
|
|
- * 用途:配送方式配置业务逻辑类
|
|
|
- * 谁用:供货商系统
|
|
|
- * 解决什么问题:处理配送方式的查询、保存、默认初始化等业务逻辑
|
|
|
+ * 用途:配送方式配置业务逻辑类(xhShMethod)
|
|
|
+ * 谁用:供货商 ghsApp 配送方式设置、花店采购下单附加费
|
|
|
+ * 解决什么问题:读写配送配置;附加费支持按自然日或冷却秒重收
|
|
|
*/
|
|
|
|
|
|
namespace bizGhs\order\classes;
|
|
|
@@ -255,8 +255,8 @@ class ShMethodClass extends BaseClass
|
|
|
|
|
|
// 不满最低消费且不允许下单
|
|
|
if ($unMeet === 2) {
|
|
|
- // 如果当天该配送方式已成功下过单,不再限制,直接放行
|
|
|
- if (self::isPackPaidToday($customId, $style) || self::isSendCostPaidToday($customId, $style)) {
|
|
|
+ // 「不能买」只认自然日成功单标记,与附加费重收间隔配置无关
|
|
|
+ if (self::isSendCostPaidToday($customId, $style) || self::isPackPaidToday($customId, $style)) {
|
|
|
return '';
|
|
|
}
|
|
|
if ($minAmount > 0 && (float)$actPrice < $minAmount) {
|
|
|
@@ -272,15 +272,14 @@ class ShMethodClass extends BaseClass
|
|
|
if (($unMeet === 0 || $unMeet === 1) && $unMeetFee > 0) {
|
|
|
// unMeet === 0 加收运费, unMeet === 1 加收包装费
|
|
|
$feeToCheck = $unMeet === 1 ? $packCost : $sendCost;
|
|
|
-
|
|
|
- // 如果今天已经收过对应的附加费用,则直接通过校验
|
|
|
- if ($unMeet === 1 && self::isPackPaidToday($customId, $style)) {
|
|
|
+
|
|
|
+ if ($unMeet === 1 && self::isUnMeetPackFeeLocked($customId, $style, $config)) {
|
|
|
return '';
|
|
|
}
|
|
|
- if ($unMeet === 0 && self::isSendCostPaidToday($customId, $style)) {
|
|
|
+ if ($unMeet === 0 && self::isUnMeetSendFeeLocked($customId, $style, $config)) {
|
|
|
return '';
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (bccomp((string)$feeToCheck, (string)$unMeetFee, 2) < 0) {
|
|
|
$feeLabel = $unMeet === 1 ? '包装费' : '运费';
|
|
|
return "未达最低消费,需加收{$feeLabel}{$unMeetFee}元";
|
|
|
@@ -290,9 +289,167 @@ class ShMethodClass extends BaseClass
|
|
|
return '';
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 规范化冷却秒数(至少 1 秒;非法则回退默认 1800)
|
|
|
+ * @param mixed $seconds
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ public static function normalizeUnMeetLockSeconds($seconds)
|
|
|
+ {
|
|
|
+ $n = (int)$seconds;
|
|
|
+ if ($n <= 0) {
|
|
|
+ return 1800;
|
|
|
+ }
|
|
|
+ return $n;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 xhShMethod 配置解析附加费重收策略
|
|
|
+ * @param array|null $config
|
|
|
+ * @return array ['type'=>0|1, 'seconds'=>int]
|
|
|
+ */
|
|
|
+ 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];
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 间隔 Redis key 至少保留 7 天,便于改配置后仍能读到收款时间 */
|
|
|
+ const UNMEET_COOLDOWN_REDIS_KEEP_SECONDS = 604800;
|
|
|
+
|
|
|
+ protected static function buildSendCostCooldownKey($sendType, $customId)
|
|
|
+ {
|
|
|
+ return 'ghs_custom_has_get_send_cost_interval_' . (int)$sendType . '_' . (int)$customId;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function buildPackCostCooldownKey($sendType, $customId)
|
|
|
+ {
|
|
|
+ return 'ghs_custom_has_get_pack_cost_interval_' . (int)$sendType . '_' . (int)$customId;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function buildDaySendPaidKey($sendType, $customId)
|
|
|
+ {
|
|
|
+ return 'ghs_custom_today_has_get_send_cost_' . (int)$sendType . '_' . date('Y_m_d') . '_' . (int)$customId;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function buildDayPackPaidKey($sendType, $customId)
|
|
|
+ {
|
|
|
+ return 'ghs_custom_today_has_get_pack_cost_' . (int)$sendType . '_' . date('Y_m_d') . '_' . (int)$customId;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function resolveCooldownRedisTtl($lockSeconds)
|
|
|
+ {
|
|
|
+ return max((int)$lockSeconds, self::UNMEET_COOLDOWN_REDIS_KEEP_SECONDS, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 间隔模式是否仍在冷却:仅认收款时间戳 + 当前配置秒数
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+ if ($val > 1000000000) {
|
|
|
+ return (time() - $val) < $expected;
|
|
|
+ }
|
|
|
+ // 旧格式无时间戳:不阻挡再收
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 运费附加费是否锁定
|
|
|
+ * type=1 只按间隔秒;type=0 自然日 + 今日间隔痕迹互认
|
|
|
+ */
|
|
|
+ public static function isUnMeetSendFeeLocked($customId, $sendType = 0, $config = null)
|
|
|
+ {
|
|
|
+ $customId = (int)$customId;
|
|
|
+ if ($customId <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $lock = self::resolveUnMeetLockOptions($config);
|
|
|
+ if ($lock['type'] === 1) {
|
|
|
+ return self::isSendCostInCooldown($customId, $sendType, $lock['seconds']);
|
|
|
+ }
|
|
|
+ if (self::isSendCostPaidToday($customId, $sendType)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return self::hasIntervalSendFeeCollectedToday($customId, $sendType);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 包装附加费是否锁定(ghs unMeet=1 为包装费)
|
|
|
+ */
|
|
|
+ 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::isPackPaidToday($customId, $sendType)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return self::hasIntervalPackFeeCollectedToday($customId, $sendType);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function hasIntervalSendFeeCollectedToday($customId, $sendType)
|
|
|
+ {
|
|
|
+ return self::hasIntervalFeeCollectedToday(self::buildSendCostCooldownKey($sendType, $customId));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function hasIntervalPackFeeCollectedToday($customId, $sendType)
|
|
|
+ {
|
|
|
+ return self::hasIntervalFeeCollectedToday(self::buildPackCostCooldownKey($sendType, $customId));
|
|
|
+ }
|
|
|
+
|
|
|
+ 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');
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 判断当天是否已经收过附加费(包装费)
|
|
|
- * 与其他逻辑共用同一个 Redis Key,保证一天只收一次
|
|
|
* @param int $customId 客户ID
|
|
|
* @param int $sendType 配送方式 (0: 送货, 4: 快递)
|
|
|
* @return bool
|
|
|
@@ -302,9 +459,7 @@ class ShMethodClass extends BaseClass
|
|
|
if (empty($customId)) {
|
|
|
return false;
|
|
|
}
|
|
|
- $current = date("Y_m_d");
|
|
|
- $key = 'ghs_custom_today_has_get_pack_cost_' . $sendType . '_' . $current . '_' . $customId;
|
|
|
- $has = \Yii::$app->redis->executeCommand('GET', [$key]);
|
|
|
+ $has = Yii::$app->redis->executeCommand('GET', [self::buildDayPackPaidKey($sendType, $customId)]);
|
|
|
return (!empty($has) && $has == 1);
|
|
|
}
|
|
|
|
|
|
@@ -319,29 +474,59 @@ class ShMethodClass extends BaseClass
|
|
|
if (empty($customId)) {
|
|
|
return false;
|
|
|
}
|
|
|
- $current = date("Y_m_d");
|
|
|
- $key = 'ghs_custom_today_has_get_send_cost_' . $sendType . '_' . $current . '_' . $customId;
|
|
|
- $has = \Yii::$app->redis->executeCommand('GET', [$key]);
|
|
|
+ $has = Yii::$app->redis->executeCommand('GET', [self::buildDaySendPaidKey($sendType, $customId)]);
|
|
|
return (!empty($has) && $has == 1);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 采购支付/欠款成功后标记附加费锁定(自然日 key + 实收时的间隔时间戳双写)
|
|
|
+ * @param int $customId
|
|
|
+ * @param int $sendType
|
|
|
+ * @param float $sendCost 本单运费附加费
|
|
|
+ * @param float $packCost 本单包装附加费
|
|
|
+ * @param array|null $config xhShMethod,可空
|
|
|
+ */
|
|
|
+ public static function markFeeStateAfterPurchaseSuccess($customId, $sendType, $sendCost = 0, $packCost = 0, $config = null)
|
|
|
+ {
|
|
|
+ $customId = (int)$customId;
|
|
|
+ if ($customId <= 0 || $sendType === null || $sendType === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $style = (int)$sendType;
|
|
|
+ $lock = self::resolveUnMeetLockOptions($config);
|
|
|
+ $lockedAt = (string)time();
|
|
|
+ $intervalTtl = self::resolveCooldownRedisTtl(max(1, (int)($lock['seconds'] ?? 1800)));
|
|
|
+ $dayTtl = 86400;
|
|
|
+
|
|
|
+ // 兼容原逻辑:成功单即记自然日已收(含不能买门槛放行)
|
|
|
+ Yii::$app->redis->executeCommand('SETEX', [self::buildDayPackPaidKey($style, $customId), $dayTtl, '1']);
|
|
|
+ Yii::$app->redis->executeCommand('SETEX', [self::buildDaySendPaidKey($style, $customId), $dayTtl, '1']);
|
|
|
+
|
|
|
+ // 实收附加费时再写间隔时间戳,供 type=1 按时长冷却
|
|
|
+ if ((float)$sendCost > 0) {
|
|
|
+ Yii::$app->redis->executeCommand('SETEX', [self::buildSendCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
|
|
|
+ }
|
|
|
+ if ((float)$packCost > 0) {
|
|
|
+ Yii::$app->redis->executeCommand('SETEX', [self::buildPackCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 计算采购单应写入的附加费用(不满最低消费时按 unMeet 加收运费或包装费)
|
|
|
* 供 createPurchase 前写入 sendCost 和 packCost,与 hdApp syncPackCostByMethod 一致
|
|
|
*
|
|
|
- * @param int $shopId 供货商门店 ID
|
|
|
- * @param int $mainId 供货商商户 ID
|
|
|
+ * @param array $config
|
|
|
* @param int $sendType 配送方式
|
|
|
- * @param float $itemTotalAmount 花材合计金额(下单前,与前端 commodityPrice 一致)
|
|
|
+ * @param float $itemTotalAmount 花材合计金额
|
|
|
* @param int|float $bigNum 下单扎数
|
|
|
- * @param int $customId 客户ID,用于判断当天是否已收过
|
|
|
+ * @param int $customId 客户ID
|
|
|
* @param array|object $ghsInfo 客户与该供货商的关系信息(xhGhs)
|
|
|
* @return array ['sendCost' => 运费, 'packCost' => 包装费]
|
|
|
*/
|
|
|
public static function calcFee($config, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $ghsInfo = [])
|
|
|
{
|
|
|
$costs = ['sendCost' => 0, 'packCost' => 0];
|
|
|
-
|
|
|
+
|
|
|
$style = (int)$sendType;
|
|
|
if ($style < 0 || $style > 4) {
|
|
|
return $costs;
|
|
|
@@ -352,7 +537,6 @@ class ShMethodClass extends BaseClass
|
|
|
}
|
|
|
|
|
|
if ($style === 0 && !empty($ghsInfo) && isset($ghsInfo['homeRule']) && $ghsInfo['homeRule'] == 1) {
|
|
|
- // 如果 homeRule == 1,特定字段取xhGhs的设置(优先级最高),其余(如name)保留原配置
|
|
|
$config['status'] = $ghsInfo['home'] ?? 0;
|
|
|
$config['minAmount'] = $ghsInfo['homeAmount'] ?? 0;
|
|
|
$config['minNum'] = $ghsInfo['homeNum'] ?? 0;
|
|
|
@@ -360,11 +544,9 @@ class ShMethodClass extends BaseClass
|
|
|
$config['unMeetFee'] = $ghsInfo['homeUnFee'] ?? 0;
|
|
|
}
|
|
|
|
|
|
- // 直接合并 isBelowMinimum 的逻辑,方便理解,不再套用额外的方法
|
|
|
$minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00;
|
|
|
$minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0;
|
|
|
-
|
|
|
- // 必须金额和数量同时满足(如果设置了的话),才算“达标”
|
|
|
+
|
|
|
$isBelowMinimum = false;
|
|
|
if ($minAmount > 0 && $itemTotalAmount < $minAmount) {
|
|
|
$isBelowMinimum = true;
|
|
|
@@ -373,22 +555,21 @@ class ShMethodClass extends BaseClass
|
|
|
$isBelowMinimum = true;
|
|
|
}
|
|
|
|
|
|
- // 如果已经达标,不收取附加费用
|
|
|
if (!$isBelowMinimum) {
|
|
|
return $costs;
|
|
|
}
|
|
|
|
|
|
$unMeet = isset($config['unMeet']) ? (int)$config['unMeet'] : 0;
|
|
|
$unMeetFee = isset($config['unMeetFee']) ? (float)$config['unMeetFee'] : 0;
|
|
|
-
|
|
|
+
|
|
|
if ($unMeetFee > 0) {
|
|
|
- // unMeet: 0 加收运费(默认),1 加收包装费,2 不能下单
|
|
|
+ // unMeet: 0 加收运费,1 加收包装费,2 不能下单
|
|
|
if ($unMeet === 0) {
|
|
|
- if (!self::isSendCostPaidToday($customId, $style)) {
|
|
|
+ if (!self::isUnMeetSendFeeLocked($customId, $style, $config)) {
|
|
|
$costs['sendCost'] = $unMeetFee;
|
|
|
}
|
|
|
} elseif ($unMeet === 1) {
|
|
|
- if (!self::isPackPaidToday($customId, $style)) {
|
|
|
+ if (!self::isUnMeetPackFeeLocked($customId, $style, $config)) {
|
|
|
$costs['packCost'] = $unMeetFee;
|
|
|
}
|
|
|
}
|
|
|
@@ -430,6 +611,9 @@ class ShMethodClass extends BaseClass
|
|
|
'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),
|
|
|
'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,
|