ShMethodClass.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <?php
  2. /**
  3. * 用途:配送方式配置业务逻辑类(xhShMethod)
  4. * 谁用:供货商 ghsApp 配送方式设置、花店采购下单附加费
  5. * 解决什么问题:读写配送配置;附加费支持按自然日或冷却秒重收
  6. */
  7. namespace bizGhs\order\classes;
  8. use bizGhs\base\classes\BaseClass;
  9. use common\components\dict;
  10. use Yii;
  11. class ShMethodClass extends BaseClass
  12. {
  13. public static $baseFile = '\bizGhs\order\models\ShMethod';
  14. /**
  15. * 获取所有配送方式的排序和别名信息
  16. * @param int $mainId 商户ID
  17. * @return array
  18. */
  19. public static function getSorts($mainId)
  20. {
  21. $list = [];
  22. $aliases = [
  23. 0 => '送货',
  24. 1 => '自取',
  25. 2 => '跑腿',
  26. 3 => '物流',
  27. 4 => '快递'
  28. ];
  29. for ($style = 0; $style <= 4; $style++) {
  30. $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]);
  31. $sort = 0;
  32. $alias = $aliases[$style];
  33. $status = 1;
  34. if (!empty($config)) {
  35. $sort = isset($config['sort']) ? (int)$config['sort'] : 0;
  36. $alias = !empty($config['name']) ? $config['name'] : $aliases[$style];
  37. $status = isset($config['status']) ? (int)$config['status'] : 1;
  38. }
  39. $list[] = [
  40. 'style' => $style,
  41. 'name' => $alias,
  42. 'sort' => $sort,
  43. 'status' => $status
  44. ];
  45. }
  46. return $list;
  47. }
  48. /**
  49. * 获取指定门店和类型的配送方式配置,若不存在则进行默认初始化
  50. * @param int $shopId 门店ID
  51. * @param int $mainId 商户ID
  52. * @param int $style 配送类型 (0: 送货, 1: 自取, 2: 跑腿, 3: 物流, 4: 快递)
  53. * @return array
  54. */
  55. public static function getConfig($shopId, $mainId, $style)
  56. {
  57. // 复杂分支/关键逻辑:优化。后端的查询全部由 shopId 改为 mainId,只根据 mainId 和 style 进行查询。
  58. $config = self::getByCondition(['mainId' => $mainId, 'style' => $style]);
  59. if (empty($config)) {
  60. // 从字典配置中读取对应类型的默认配置
  61. $defaultConfig = dict::getDict('shMethod', $style);
  62. $initData = array_merge([
  63. 'shopId' => $shopId, // shopId 仅作为保存预留字段存入
  64. 'mainId' => $mainId,
  65. 'style' => $style,
  66. ], $defaultConfig);
  67. $config = self::add($initData, true);
  68. $config = $config->toArray();
  69. }
  70. // 获取关联的说明项
  71. $config['explains'] = ShExplainClass::getAllByCondition(
  72. ['methodId' => $config['id']],
  73. 'sort ASC, id ASC'
  74. );
  75. // 如果 is_跑腿 (style为2),获取关联的满减规则
  76. if ($style == 2) {
  77. $config['reduceRules'] = ShReduceRuleClass::getAllByCondition(
  78. ['methodId' => $config['id']],
  79. 'sort ASC, id ASC'
  80. );
  81. } else {
  82. $config['reduceRules'] = [];
  83. }
  84. return $config;
  85. }
  86. /**
  87. * 一次性获取商户所有的5种配送方式配置,减少数据库查询交互。
  88. * 如果数据库里没有对应配置,则自动采用 dict.php 里的默认参数进行填充并完成入库。
  89. * @param int $shopId 门店ID
  90. * @param int $mainId 商户ID
  91. * @return array
  92. */
  93. public static function getAllConfigs($shopId, $mainId)
  94. {
  95. // 1. 一次性从数据库中查询当前商户的所有配送方式配置
  96. $configsMap = self::getAllByCondition(['mainId' => $mainId], 'sort asc', '*', 'style', true);
  97. // 2. 补齐缺失的配送方式,并做初始化入库
  98. for ($style = 0; $style <= 4; $style++) {
  99. if (!isset($configsMap[$style])) {
  100. $defaultConfig = dict::getDict('shMethod', $style);
  101. $initData = array_merge([
  102. 'shopId' => $shopId, // shopId 仅作为保存预留字段存入
  103. 'mainId' => $mainId,
  104. 'style' => $style,
  105. ], $defaultConfig);
  106. $configObj = self::add($initData, true);
  107. $configsMap[$style] = $configObj;
  108. }
  109. }
  110. // 将 ActiveRecord 统一转换为 array,并提取 methodIds 用于批量子表查询
  111. $configsArray = [];
  112. $methodIds = [];
  113. foreach ($configsMap as $style => $configObj) {
  114. $configArr = $configObj instanceof \yii\db\ActiveRecord ? $configObj->toArray() : $configObj;
  115. $configsArray[$style] = $configArr;
  116. $methodIds[] = (int)$configArr['id'];
  117. }
  118. // 3. 批量查询所有的说明项 (xhShExplain) - 仅 1 次查询
  119. $allExplains = [];
  120. if (!empty($methodIds)) {
  121. $allExplains = ShExplainClass::getAllByCondition(['methodId' => ['in', $methodIds]], 'sort ASC, id ASC');
  122. }
  123. $explainsMap = [];
  124. foreach ($allExplains as $exp) {
  125. $explainsMap[(int)$exp['methodId']][] = $exp;
  126. }
  127. // 4. 批量查询所有的跑腿满减规则 (xhShReduceRule) - 仅 1 次查询
  128. $allReduceRules = [];
  129. if (!empty($methodIds)) {
  130. $allReduceRules = ShReduceRuleClass::getAllByCondition(['methodId' => ['in', $methodIds]], 'sort ASC, id ASC');
  131. }
  132. $reduceRulesMap = [];
  133. foreach ($allReduceRules as $rule) {
  134. $reduceRulesMap[(int)$rule['methodId']][] = $rule;
  135. }
  136. // 5. 将批量查出的说明项和规则,合并填充到 5 个配送方式中
  137. $result = [];
  138. for ($style = 0; $style <= 4; $style++) {
  139. $config = $configsArray[$style];
  140. $methodId = (int)$config['id'];
  141. $config['explains'] = $explainsMap[$methodId] ?? [];
  142. $config['reduceRules'] = ($style == 2) ? ($reduceRulesMap[$methodId] ?? []) : [];
  143. $result[] = $config;
  144. }
  145. // 按 sort 升序返回,供 hd/ghs GetAllConfig 等接口展示自定义排序
  146. usort($result, function ($a, $b) {
  147. $sortCompare = (int)($a['sort'] ?? 0) <=> (int)($b['sort'] ?? 0);
  148. if ($sortCompare !== 0) {
  149. return $sortCompare;
  150. }
  151. // sort 相同时按 style 稳定排序,避免顺序抖动
  152. return (int)($a['style'] ?? 0) <=> (int)($b['style'] ?? 0);
  153. });
  154. return $result;
  155. }
  156. /**
  157. * 判断配送方式配置是否未达最低消费(金额或扎数任一不满足即视为未达标)
  158. * 与 hdApp affirm-sh-method-panel.checkShMethodBelowMinimum 规则一致
  159. *
  160. * @param array $config xhShMethod 配置
  161. * @param float $actPrice 实付花材金额
  162. * @param int|float $bigNum 下单扎数
  163. * @return bool
  164. */
  165. /**
  166. * 采购下单后校验 xhShMethod 配送限制
  167. * 供 hd PurchaseController::actionCreateOrder 在 createPurchase 之后调用,与旧版 else 分支门店硬编码限制同级
  168. *
  169. * @param int $shopId 供货商门店 ID
  170. * @param int $mainId 供货商商户 ID
  171. * @param int $sendType 配送方式,与 xhShMethod.style 一致
  172. * @param float $actPrice 下单后实付花材金额
  173. * @param int|float $bigNum 下单扎数
  174. * @param float $packCost 已写入采购单的包装费
  175. * @param float $sendCost 已写入采购单的运费
  176. * @param string $wlName 物流公司名称(style=3 时必填)
  177. * @param int $customId 客户ID,用于判断当天是否已收过
  178. * @param array|object $ghsInfo 客户与该供货商的关系信息(xhGhs)
  179. * @return string 空字符串表示通过,否则为需提示用户的错误文案
  180. */
  181. public static function checkLimit($shopId, $mainId, $sendType, $actPrice, $bigNum, $packCost = 0, $sendCost = 0, $wlName = '', $customId = 0, $ghsInfo = [])
  182. {
  183. $style = (int)$sendType;
  184. if ($style < 0 || $style > 4) {
  185. return '配送方式无效';
  186. }
  187. $config = self::getConfig($shopId, $mainId, $style);
  188. if (empty($config)) {
  189. return '配送方式配置异常';
  190. }
  191. if ($style === 0 && !empty($ghsInfo) && isset($ghsInfo['homeRule']) && $ghsInfo['homeRule'] == 1) {
  192. // 如果 homeRule == 1,特定字段取xhGhs的设置(优先级最高),其余(如name)保留原配置
  193. $config['status'] = $ghsInfo['home'] ?? 0;
  194. $config['minAmount'] = $ghsInfo['homeAmount'] ?? 0;
  195. $config['minNum'] = $ghsInfo['homeNum'] ?? 0;
  196. $config['unMeet'] = $ghsInfo['homeUnMeet'] ?? 0;
  197. $config['unMeetFee'] = $ghsInfo['homeUnFee'] ?? 0;
  198. }
  199. $methodName = !empty($config['name']) ? $config['name'] : '该配送方式';
  200. // 配送方式已禁用,对齐旧逻辑「暂不支持送货上门,请选其它配送方式」
  201. if (isset($config['status']) && (int)$config['status'] === 0) {
  202. return "暂不支持{$methodName},请选其它配送方式";
  203. }
  204. // 物流需填物流公司,与 hdApp validateBeforeSubmit 一致
  205. if ($style === 3 && trim((string)$wlName) === '') {
  206. return '请填选物流';
  207. }
  208. $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00;
  209. $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0;
  210. // 必须金额和数量同时满足(如果设置了的话),才算“达标”
  211. $isBelowMinimum = false;
  212. if ($minAmount > 0 && $actPrice < $minAmount) {
  213. $isBelowMinimum = true;
  214. }
  215. if ($minNum > 0 && (float)$bigNum < $minNum) {
  216. $isBelowMinimum = true;
  217. }
  218. if (!$isBelowMinimum) {
  219. return '';
  220. }
  221. $unMeet = isset($config['unMeet']) ? (int)$config['unMeet'] : 0;
  222. $unMeetFee = isset($config['unMeetFee']) ? (float)$config['unMeetFee'] : 0;
  223. // 不满最低消费且不允许下单
  224. if ($unMeet === 2) {
  225. // 「不能买」只认自然日成功单标记,与附加费重收间隔配置无关
  226. if (self::isSendCostPaidToday($customId, $style) || self::isPackPaidToday($customId, $style)) {
  227. return '';
  228. }
  229. if ($minAmount > 0 && (float)$actPrice < $minAmount) {
  230. return "满{$minAmount}元 可{$methodName}";
  231. }
  232. if ($minNum > 0 && (float)$bigNum < $minNum) {
  233. return "满{$minNum}扎 可{$methodName}";
  234. }
  235. return '未达到最低消费,不能下单';
  236. }
  237. // 不满最低消费但允许加收费用时,校验包装费/运费是否已写入采购单,防止前端绕过
  238. if (($unMeet === 0 || $unMeet === 1) && $unMeetFee > 0) {
  239. // unMeet === 0 加收运费, unMeet === 1 加收包装费
  240. $feeToCheck = $unMeet === 1 ? $packCost : $sendCost;
  241. if ($unMeet === 1 && self::isUnMeetPackFeeLocked($customId, $style, $config)) {
  242. return '';
  243. }
  244. if ($unMeet === 0 && self::isUnMeetSendFeeLocked($customId, $style, $config)) {
  245. return '';
  246. }
  247. if (bccomp((string)$feeToCheck, (string)$unMeetFee, 2) < 0) {
  248. $feeLabel = $unMeet === 1 ? '包装费' : '运费';
  249. return "未达最低消费,需加收{$feeLabel}{$unMeetFee}元";
  250. }
  251. }
  252. return '';
  253. }
  254. /**
  255. * 规范化冷却秒数(至少 1 秒;非法则回退默认 1800)
  256. * @param mixed $seconds
  257. * @return int
  258. */
  259. public static function normalizeUnMeetLockSeconds($seconds)
  260. {
  261. $n = (int)$seconds;
  262. if ($n <= 0) {
  263. return 1800;
  264. }
  265. return $n;
  266. }
  267. /**
  268. * 从 xhShMethod 配置解析附加费重收策略
  269. * @param array|null $config
  270. * @return array ['type'=>0|1, 'seconds'=>int]
  271. */
  272. public static function resolveUnMeetLockOptions($config = null)
  273. {
  274. $type = 0;
  275. $seconds = 1800;
  276. if (is_array($config)) {
  277. $type = (int)($config['unMeetLockType'] ?? 0);
  278. $seconds = self::normalizeUnMeetLockSeconds($config['unMeetLockSeconds'] ?? 1800);
  279. }
  280. if ($type !== 1) {
  281. $type = 0;
  282. }
  283. return ['type' => $type, 'seconds' => $seconds];
  284. }
  285. /** 间隔 Redis key 至少保留 7 天,便于改配置后仍能读到收款时间 */
  286. const UNMEET_COOLDOWN_REDIS_KEEP_SECONDS = 604800;
  287. protected static function buildSendCostCooldownKey($sendType, $customId)
  288. {
  289. return 'ghs_custom_has_get_send_cost_interval_' . (int)$sendType . '_' . (int)$customId;
  290. }
  291. protected static function buildPackCostCooldownKey($sendType, $customId)
  292. {
  293. return 'ghs_custom_has_get_pack_cost_interval_' . (int)$sendType . '_' . (int)$customId;
  294. }
  295. protected static function buildDaySendPaidKey($sendType, $customId)
  296. {
  297. return 'ghs_custom_today_has_get_send_cost_' . (int)$sendType . '_' . date('Y_m_d') . '_' . (int)$customId;
  298. }
  299. protected static function buildDayPackPaidKey($sendType, $customId)
  300. {
  301. return 'ghs_custom_today_has_get_pack_cost_' . (int)$sendType . '_' . date('Y_m_d') . '_' . (int)$customId;
  302. }
  303. protected static function resolveCooldownRedisTtl($lockSeconds)
  304. {
  305. return max((int)$lockSeconds, self::UNMEET_COOLDOWN_REDIS_KEEP_SECONDS, 1);
  306. }
  307. /**
  308. * 间隔模式是否仍在冷却:仅认收款时间戳 + 当前配置秒数
  309. */
  310. protected static function isIntervalCooldownActive($key, $expectedSeconds)
  311. {
  312. $has = Yii::$app->redis->executeCommand('GET', [$key]);
  313. if ($has === null || $has === false || $has === '') {
  314. return false;
  315. }
  316. $expected = max(1, (int)$expectedSeconds);
  317. $val = (int)$has;
  318. if ($val > 1000000000) {
  319. return (time() - $val) < $expected;
  320. }
  321. // 旧格式无时间戳:不阻挡再收
  322. return false;
  323. }
  324. public static function isSendCostInCooldown($customId, $sendType = 0, $expectedSeconds = null)
  325. {
  326. if (empty($customId)) {
  327. return false;
  328. }
  329. $seconds = $expectedSeconds !== null ? (int)$expectedSeconds : 1800;
  330. return self::isIntervalCooldownActive(self::buildSendCostCooldownKey($sendType, $customId), $seconds);
  331. }
  332. public static function isPackCostInCooldown($customId, $sendType = 0, $expectedSeconds = null)
  333. {
  334. if (empty($customId)) {
  335. return false;
  336. }
  337. $seconds = $expectedSeconds !== null ? (int)$expectedSeconds : 1800;
  338. return self::isIntervalCooldownActive(self::buildPackCostCooldownKey($sendType, $customId), $seconds);
  339. }
  340. /**
  341. * 运费附加费是否锁定
  342. * type=1 只按间隔秒;type=0 自然日 + 今日间隔痕迹互认
  343. */
  344. public static function isUnMeetSendFeeLocked($customId, $sendType = 0, $config = null)
  345. {
  346. $customId = (int)$customId;
  347. if ($customId <= 0) {
  348. return false;
  349. }
  350. $lock = self::resolveUnMeetLockOptions($config);
  351. if ($lock['type'] === 1) {
  352. return self::isSendCostInCooldown($customId, $sendType, $lock['seconds']);
  353. }
  354. if (self::isSendCostPaidToday($customId, $sendType)) {
  355. return true;
  356. }
  357. return self::hasIntervalSendFeeCollectedToday($customId, $sendType);
  358. }
  359. /**
  360. * 包装附加费是否锁定(ghs unMeet=1 为包装费)
  361. */
  362. public static function isUnMeetPackFeeLocked($customId, $sendType = 0, $config = null)
  363. {
  364. $customId = (int)$customId;
  365. if ($customId <= 0) {
  366. return false;
  367. }
  368. $lock = self::resolveUnMeetLockOptions($config);
  369. if ($lock['type'] === 1) {
  370. return self::isPackCostInCooldown($customId, $sendType, $lock['seconds']);
  371. }
  372. if (self::isPackPaidToday($customId, $sendType)) {
  373. return true;
  374. }
  375. return self::hasIntervalPackFeeCollectedToday($customId, $sendType);
  376. }
  377. protected static function hasIntervalSendFeeCollectedToday($customId, $sendType)
  378. {
  379. return self::hasIntervalFeeCollectedToday(self::buildSendCostCooldownKey($sendType, $customId));
  380. }
  381. protected static function hasIntervalPackFeeCollectedToday($customId, $sendType)
  382. {
  383. return self::hasIntervalFeeCollectedToday(self::buildPackCostCooldownKey($sendType, $customId));
  384. }
  385. protected static function hasIntervalFeeCollectedToday($key)
  386. {
  387. $has = Yii::$app->redis->executeCommand('GET', [$key]);
  388. if ($has === null || $has === false || $has === '') {
  389. return false;
  390. }
  391. $val = (int)$has;
  392. if ($val > 1000000000) {
  393. return date('Y-m-d', $val) === date('Y-m-d');
  394. }
  395. return true;
  396. }
  397. /**
  398. * 判断当天是否已经收过附加费(包装费)
  399. * @param int $customId 客户ID
  400. * @param int $sendType 配送方式 (0: 送货, 4: 快递)
  401. * @return bool
  402. */
  403. public static function isPackPaidToday($customId, $sendType = 0)
  404. {
  405. if (empty($customId)) {
  406. return false;
  407. }
  408. $has = Yii::$app->redis->executeCommand('GET', [self::buildDayPackPaidKey($sendType, $customId)]);
  409. return (!empty($has) && $has == 1);
  410. }
  411. /**
  412. * 判断当天是否已经收过运费
  413. * @param int $customId 客户ID
  414. * @param int $sendType 配送方式
  415. * @return bool
  416. */
  417. public static function isSendCostPaidToday($customId, $sendType = 0)
  418. {
  419. if (empty($customId)) {
  420. return false;
  421. }
  422. $has = Yii::$app->redis->executeCommand('GET', [self::buildDaySendPaidKey($sendType, $customId)]);
  423. return (!empty($has) && $has == 1);
  424. }
  425. /**
  426. * 采购支付/欠款成功后标记附加费锁定(自然日 key + 实收时的间隔时间戳双写)
  427. * @param int $customId
  428. * @param int $sendType
  429. * @param float $sendCost 本单运费附加费
  430. * @param float $packCost 本单包装附加费
  431. * @param array|null $config xhShMethod,可空
  432. */
  433. public static function markFeeStateAfterPurchaseSuccess($customId, $sendType, $sendCost = 0, $packCost = 0, $config = null)
  434. {
  435. $customId = (int)$customId;
  436. if ($customId <= 0 || $sendType === null || $sendType === '') {
  437. return;
  438. }
  439. $style = (int)$sendType;
  440. $lock = self::resolveUnMeetLockOptions($config);
  441. $lockedAt = (string)time();
  442. $intervalTtl = self::resolveCooldownRedisTtl(max(1, (int)($lock['seconds'] ?? 1800)));
  443. $dayTtl = 86400;
  444. // 兼容原逻辑:成功单即记自然日已收(含不能买门槛放行)
  445. Yii::$app->redis->executeCommand('SETEX', [self::buildDayPackPaidKey($style, $customId), $dayTtl, '1']);
  446. Yii::$app->redis->executeCommand('SETEX', [self::buildDaySendPaidKey($style, $customId), $dayTtl, '1']);
  447. // 实收附加费时再写间隔时间戳,供 type=1 按时长冷却
  448. if ((float)$sendCost > 0) {
  449. Yii::$app->redis->executeCommand('SETEX', [self::buildSendCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
  450. }
  451. if ((float)$packCost > 0) {
  452. Yii::$app->redis->executeCommand('SETEX', [self::buildPackCostCooldownKey($style, $customId), $intervalTtl, $lockedAt]);
  453. }
  454. }
  455. /**
  456. * 计算采购单应写入的附加费用(不满最低消费时按 unMeet 加收运费或包装费)
  457. * 供 createPurchase 前写入 sendCost 和 packCost,与 hdApp syncPackCostByMethod 一致
  458. *
  459. * @param array $config
  460. * @param int $sendType 配送方式
  461. * @param float $itemTotalAmount 花材合计金额
  462. * @param int|float $bigNum 下单扎数
  463. * @param int $customId 客户ID
  464. * @param array|object $ghsInfo 客户与该供货商的关系信息(xhGhs)
  465. * @return array ['sendCost' => 运费, 'packCost' => 包装费]
  466. */
  467. public static function calcFee($config, $sendType, $itemTotalAmount, $bigNum, $customId = 0, $ghsInfo = [])
  468. {
  469. $costs = ['sendCost' => 0, 'packCost' => 0];
  470. $style = (int)$sendType;
  471. if ($style < 0 || $style > 4) {
  472. return $costs;
  473. }
  474. if (empty($config)) {
  475. return $costs;
  476. }
  477. if ($style === 0 && !empty($ghsInfo) && isset($ghsInfo['homeRule']) && $ghsInfo['homeRule'] == 1) {
  478. $config['status'] = $ghsInfo['home'] ?? 0;
  479. $config['minAmount'] = $ghsInfo['homeAmount'] ?? 0;
  480. $config['minNum'] = $ghsInfo['homeNum'] ?? 0;
  481. $config['unMeet'] = $ghsInfo['homeUnMeet'] ?? 0;
  482. $config['unMeetFee'] = $ghsInfo['homeUnFee'] ?? 0;
  483. }
  484. $minAmount = isset($config['minAmount']) ? (float)$config['minAmount'] : 0.00;
  485. $minNum = isset($config['minNum']) ? (int)$config['minNum'] : 0;
  486. $isBelowMinimum = false;
  487. if ($minAmount > 0 && $itemTotalAmount < $minAmount) {
  488. $isBelowMinimum = true;
  489. }
  490. if ($minNum > 0 && (float)$bigNum < $minNum) {
  491. $isBelowMinimum = true;
  492. }
  493. if (!$isBelowMinimum) {
  494. return $costs;
  495. }
  496. $unMeet = isset($config['unMeet']) ? (int)$config['unMeet'] : 0;
  497. $unMeetFee = isset($config['unMeetFee']) ? (float)$config['unMeetFee'] : 0;
  498. if ($unMeetFee > 0) {
  499. // unMeet: 0 加收运费,1 加收包装费,2 不能下单
  500. if ($unMeet === 0) {
  501. if (!self::isUnMeetSendFeeLocked($customId, $style, $config)) {
  502. $costs['sendCost'] = $unMeetFee;
  503. }
  504. } elseif ($unMeet === 1) {
  505. if (!self::isUnMeetPackFeeLocked($customId, $style, $config)) {
  506. $costs['packCost'] = $unMeetFee;
  507. }
  508. }
  509. }
  510. return $costs;
  511. }
  512. /**
  513. * 保存配送方式配置,包含说明项和满减规则
  514. * @param int $shopId 门店ID
  515. * @param int $mainId 商户ID
  516. * @param array $data 配置数据
  517. * @return bool
  518. * @throws \Exception
  519. */
  520. public static function saveConfig($shopId, $mainId, $data)
  521. {
  522. $id = $data['id'] ?? 0;
  523. $style = $data['style'] ?? 0;
  524. $config = null;
  525. if ($id > 0) {
  526. // 复杂分支/关键逻辑:优化。后端的查询全部由 shopId 改为 mainId。
  527. $config = self::getByCondition(['id' => $id, 'mainId' => $mainId], true);
  528. }
  529. if (empty($config)) {
  530. // 复杂分支/关键逻辑:优化。后端的查询全部由 shopId 改为 mainId。
  531. $config = self::getByCondition(['mainId' => $mainId, 'style' => $style], true);
  532. }
  533. $saveData = [
  534. 'name' => $data['name'] ?? '',
  535. 'calcType' => isset($data['calcType']) ? (int)$data['calcType'] : 0, // 算运费方式 (0: 用跑腿, 1: 用自定义)
  536. 'status' => isset($data['status']) ? (int)$data['status'] : 1,
  537. 'sort' => isset($data['sort']) ? (int)$data['sort'] : 0,
  538. 'minAmount' => isset($data['minAmount']) ? (float)$data['minAmount'] : 0.00,
  539. 'minNum' => isset($data['minNum']) ? (int)$data['minNum'] : 0,
  540. 'unMeet' => isset($data['unMeet']) ? (int)$data['unMeet'] : 0,
  541. 'unMeetFee' => isset($data['unMeetFee']) ? (float)$data['unMeetFee'] : 0.00,
  542. // 附加费重收:0 自然日;1 冷却秒(配置页按小时录入后换算)
  543. 'unMeetLockType' => (isset($data['unMeetLockType']) && (int)$data['unMeetLockType'] === 1) ? 1 : 0,
  544. 'unMeetLockSeconds' => self::normalizeUnMeetLockSeconds($data['unMeetLockSeconds'] ?? 1800),
  545. 'startKm' => isset($data['startKm']) ? (float)$data['startKm'] : 0.00,
  546. 'startPrice' => isset($data['startPrice']) ? (float)$data['startPrice'] : 0.00,
  547. 'perKmPrice' => isset($data['perKmPrice']) ? (float)$data['perKmPrice'] : 0.00,
  548. 'changeRate' => isset($data['changeRate']) ? (float)$data['changeRate'] : 0.00,
  549. ];
  550. $transaction = Yii::$app->db->beginTransaction();
  551. try {
  552. if (empty($config)) {
  553. $saveData['shopId'] = $shopId; // shopId 作为保存预留字段存入
  554. $saveData['mainId'] = $mainId;
  555. $saveData['style'] = $style;
  556. $config = self::add($saveData, true);
  557. } else {
  558. self::updateByCondition(['id' => $config->id], $saveData);
  559. }
  560. $methodId = $config->id;
  561. // 1. 保存说明项 (xhShExplain)
  562. ShExplainClass::deleteByCondition(['methodId' => $methodId]);
  563. if (!empty($data['explains']) && is_array($data['explains'])) {
  564. $explainRows = [];
  565. foreach ($data['explains'] as $index => $exp) {
  566. if (empty($exp['explain'])) {
  567. continue;
  568. }
  569. $explainRows[] = [
  570. 'methodId' => $methodId,
  571. 'explain' => $exp['explain'],
  572. 'color' => isset($exp['color']) ? (int)$exp['color'] : 1,
  573. 'fontWeight' => isset($exp['fontWeight']) ? (int)$exp['fontWeight'] : 1,
  574. 'sort' => isset($exp['sort']) ? (int)$exp['sort'] : $index,
  575. ];
  576. }
  577. if (!empty($explainRows)) {
  578. ShExplainClass::batchAdd($explainRows);
  579. }
  580. }
  581. // 2. 保存跑腿满减规则 (xhShReduceRule)
  582. ShReduceRuleClass::deleteByCondition(['methodId' => $methodId]);
  583. if ($style == 2 && !empty($data['reduceRules']) && is_array($data['reduceRules'])) {
  584. $ruleRows = [];
  585. foreach ($data['reduceRules'] as $index => $rule) {
  586. $ruleRows[] = [
  587. 'methodId' => $methodId,
  588. 'meetNum' => isset($rule['meetNum']) ? (int)$rule['meetNum'] : 0,
  589. 'meetAmount' => isset($rule['meetAmount']) ? (float)$rule['meetAmount'] : 0.00,
  590. 'freeKm' => isset($rule['freeKm']) ? (float)$rule['freeKm'] : 0.00,
  591. 'sort' => isset($rule['sort']) ? (int)$rule['sort'] : $index,
  592. ];
  593. }
  594. if (!empty($ruleRows)) {
  595. ShReduceRuleClass::batchAdd($ruleRows);
  596. }
  597. }
  598. $transaction->commit();
  599. return true;
  600. } catch (\Exception $e) {
  601. $transaction->rollBack();
  602. throw $e;
  603. }
  604. }
  605. }