ProductClass.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. <?php
  2. namespace bizHd\product\classes;
  3. use biz\item\classes\PtItemClass;
  4. use bizHd\base\classes\BaseClass;
  5. use bizHd\custom\classes\CustomClass;
  6. use common\components\imgUtil;
  7. use common\components\noticeUtil;
  8. use common\components\sms;
  9. use common\components\util;
  10. use PhpAmqpLib\Wire\AMQPTable;
  11. use Yii;
  12. class ProductClass extends BaseClass
  13. {
  14. const LIMIT_BUY_KEY = 'hd_limit_buy:'; // 零售客户与批发客户不同,这个要与批发不同
  15. const CLEAR_MARK_KEY = 'limit_buy_clear_at:';
  16. const CLEAR_LOOP_KEY = 'limit_buy_clear_loop:';
  17. const LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY = 'limitBuyRollbackSnapshot'; //记录限购旧值快照缓存键
  18. public static $baseFile = '\bizHd\product\models\Product';
  19. //花材列表用到的数据组合 ssh 20221225
  20. public static function itemListGroup($list, $level = 0)
  21. {
  22. //各个等级对应的price addPrice字段
  23. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  24. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  25. foreach ($list as $k => $v) {
  26. $shortCover = $v['cover'] ?? '';
  27. $cover = imgUtil::getPrefix() . 'hhb_small.png';
  28. if (!empty($shortCover)) {
  29. $parse = parse_url(self::groupImg($shortCover));
  30. if (empty($parse['query'])) {
  31. $cover = self::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  32. } else {
  33. $cover = self::groupImg($shortCover);
  34. }
  35. }
  36. $list[$k]['cover'] = $cover;
  37. //今日特价、会员价
  38. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  39. if ($level == 0) {
  40. //零售做特价时显示的原价,不能删除
  41. if (isset($v['skPrice'])) {
  42. $list[$k]['prePrice'] = $v['skPrice'];
  43. }
  44. }
  45. if (isset($v['discountPrice']) && $v['discountPrice'] > 0) {
  46. //做特价花材时,花材列表和改价列表显示的会员价和零售价,不能删除
  47. $list[$k]['hjPrice'] = floatval($v['hjDiscountPrice']);
  48. $list[$k]['skPrice'] = floatval($v['skDiscountPrice']);
  49. }
  50. $list[$k]['price'] = $price;
  51. $cost = $v['cost'] ?? 0;
  52. if ($price > 0) {
  53. $profit = bcsub($price, $cost, 2);
  54. $mll = bcdiv($profit, $price, 3);
  55. $mll = bcmul($mll, 100);
  56. $mll = round($mll, 1);
  57. } else {
  58. $mll = 0;
  59. }
  60. $list[$k]['mll'] = $mll;
  61. }
  62. return $list;
  63. }
  64. public static function kdItemGroup($list, $level = 0)
  65. {
  66. //各个等级对应的price addPrice字段
  67. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  68. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  69. foreach ($list as $k => $v) {
  70. $shortCover = $v['cover'] ?? '';
  71. $cover = imgUtil::getPrefix() . 'hhb_small.png';
  72. if (!empty($shortCover)) {
  73. $parse = parse_url(self::groupImg($shortCover));
  74. if (empty($parse['query'])) {
  75. $cover = self::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  76. } else {
  77. $cover = self::groupImg($shortCover);
  78. }
  79. }
  80. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 1;
  81. $list[$k]['cover'] = $cover;
  82. $stockInfo = self::formatStock($v['stock'], $ratio);
  83. $list[$k]['bigNum'] = $stockInfo['bigNum'];
  84. $list[$k]['smallNum'] = $stockInfo['smallNum'];
  85. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  86. $list[$k]['price'] = $price;
  87. $list[$k]['bigPrice'] = $price;
  88. $smallRatio = $v['smallRatio'] ?? 0;
  89. $smallPrice = bcdiv($price, $ratio, 2);
  90. $smallPrice = bcmul($smallPrice, $smallRatio, 2);
  91. $list[$k]['smallPrice'] = $smallPrice;
  92. }
  93. return $list;
  94. }
  95. //花店的花材转成批发店花材 ssh 20230308
  96. public static function hdToggleGhs($list, $ghsShop)
  97. {
  98. $ghsMainId = $ghsShop->mainId ?? 0;
  99. $ptItemIds = array_column($list, 'itemId');
  100. $ghsHasProduct = ProductClass::getAllByCondition(['mainId' => $ghsMainId, 'itemId' => ['in', $ptItemIds]], null, '*', 'itemId');
  101. $ghsSjId = $ghsShop->sjId ?? 0;
  102. $ghsShopId = $ghsShop->id ?? 0;
  103. $defaultClassId = \bizGhs\item\classes\ItemClassClass::getDefaultClassId($ghsMainId);
  104. if (empty($defaultClassId)) {
  105. util::fail('没有找到花材的默认分类');
  106. }
  107. foreach ($list as $k => $v) {
  108. $ptItemId = $v['itemId'] ?? 0;
  109. $hdProductId = $v['productId'] ?? 0;
  110. if (isset($ghsHasProduct[$ptItemId]) == false) {
  111. //不存在,则从平台新增
  112. $ptItem = PtItemClass::getById($ptItemId);
  113. unset($ptItem['addTime']);
  114. unset($ptItem['updateTime']);
  115. $productData = $ptItem;
  116. $productData['sjId'] = $ghsSjId;
  117. $productData['shopId'] = $ghsShopId;
  118. $productData['mainId'] = $ghsMainId;
  119. $productData['itemId'] = $ptItemId;
  120. $productData['classId'] = $defaultClassId;
  121. unset($productData['id']);
  122. $productData['rank'] = '';
  123. $cost = $v['cost'] ?? 0;
  124. $addPrice = $v['addPrice'] ?? 0;
  125. $productData['price'] = bcadd($cost, $addPrice, 2);
  126. $addProduct = ProductClass::addBaseData($productData);
  127. $ghsProductId = $addProduct->id ?? 0;
  128. } else {
  129. $ghsProductId = $ghsHasProduct[$ptItemId]['id'] ?? 0;
  130. }
  131. $list[$k]['productId'] = $ghsProductId;
  132. $list[$k]['hdProductId'] = $hdProductId;
  133. }
  134. return $list;
  135. }
  136. public static function addBaseData($data)
  137. {
  138. $respond = self::add($data, true);
  139. return $respond;
  140. }
  141. public static function adStockCheck($shop, $productList)
  142. {
  143. //阿东开5支老是会出现5扎情况跟踪
  144. $mainId = $shop->mainId ?? 0;
  145. Yii::info("mainId:" . $mainId);
  146. if (getenv('YII_ENV') == 'production') {
  147. if ($mainId != 742 && $mainId != 50) {
  148. return false;
  149. }
  150. } else {
  151. if ($mainId != 644) {
  152. return false;
  153. }
  154. }
  155. Yii::info('productList:' . json_encode($productList));
  156. if (!empty($productList)) {
  157. $adIds = [];
  158. foreach ($productList as $adItem) {
  159. if (isset($adItem['property']) && $adItem['property'] == 1 && $adItem['unitType'] == 0 && $adItem['num'] == 5) {
  160. $adIds[] = $adItem['productId'];
  161. Yii::info('productId:' . $adItem['productId']);
  162. }
  163. }
  164. if (!empty($adIds)) {
  165. $arr = self::getByIds($adIds);
  166. if (!empty($arr)) {
  167. foreach ($arr as $it) {
  168. Yii::info("num:" . $it['scanNum']);
  169. if (isset($it['scanNum']) && $it['scanNum'] > 0) {
  170. $name = $it['name'] ?? '';
  171. if (getenv('YII_ENV') == 'production') {
  172. if ($mainId == 742) {
  173. sms::freeSend('13531700113,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  174. sms::freeSend('15220670540,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  175. sms::freeSend('13531704009,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  176. sms::freeSend('15766689130,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  177. sms::freeSend('15280215347,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  178. }
  179. if ($mainId == 50) {
  180. sms::freeSend('15280215347,' . $name, '刚刚,花材:{$var},5支被开成了5扎,请注意!!!');
  181. }
  182. } else {
  183. noticeUtil::push('刚刚,花材:' . $name . ',5支被开成了5扎,请注意!!!', '15280215347');
  184. }
  185. }
  186. }
  187. }
  188. }
  189. }
  190. }
  191. //花材的库存转换: 库存是小数,根据各个花材的unitNum, 计算出多少扎,多少支
  192. public static function formatStock($stock, $unitNum)
  193. {
  194. $pn = true; //正数
  195. if ($stock < 0) {
  196. $pn = false;
  197. $stock = abs($stock);
  198. }
  199. $bigNum = floor($stock); // 扎
  200. $stockArr = explode('.', $stock);
  201. $smallNum = 0; //支
  202. if (count($stockArr) === 2) {
  203. $smallNum = bcmul(($stock - $bigNum), $unitNum);
  204. }
  205. if (!$pn) {
  206. $bigNum *= -1;
  207. $smallNum *= -1;
  208. }
  209. return [
  210. 'bigNum' => $bigNum,
  211. 'smallNum' => $smallNum,
  212. ];
  213. }
  214. //通用花材列表数组组合
  215. public static function changePriceGroup($list, $level = 0)
  216. {
  217. //各个等级对应的price addPrice字段
  218. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  219. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  220. foreach ($list as $k => $v) {
  221. $shortCover = $v['cover'] ?? '';
  222. $cover = imgUtil::getPrefix() . 'hhb_small.png';
  223. if (!empty($shortCover)) {
  224. $parse = parse_url(self::groupImg($shortCover));
  225. if (empty($parse['query'])) {
  226. $cover = self::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  227. } else {
  228. $cover = self::groupImg($shortCover);
  229. }
  230. }
  231. $list[$k]['cover'] = $cover;
  232. $list[$k]['prePrice'] = $v['price'] ?? 0;
  233. //今日特价、会员价
  234. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  235. if ($level == 0) {
  236. //零售做特价时显示的原价,不能删除
  237. if (isset($v['skPrice'])) {
  238. $list[$k]['prePrice'] = $v['skPrice'];
  239. }
  240. }
  241. if (isset($v['discountPrice']) && $v['discountPrice'] > 0) {
  242. //做特价花材时,花材列表和改价列表显示的会员价和零售价,不能删除
  243. $list[$k]['hjPrice'] = floatval($v['hjDiscountPrice']);
  244. $list[$k]['skPrice'] = floatval($v['skDiscountPrice']);
  245. }
  246. $list[$k]['price'] = $price;
  247. $cost = $v['avCost'] ?? 0;
  248. $ml = 0;
  249. if ($price > 0) {
  250. $profit = bcsub($price, $cost, 2);
  251. $ml = $profit;
  252. $mll = bcdiv($profit, $price, 3);
  253. $mll = bcmul($mll, 100);
  254. $mll = round($mll, 1);
  255. } else {
  256. $mll = 0;
  257. }
  258. $list[$k]['mll'] = $mll;
  259. $list[$k]['ml'] = $ml;
  260. }
  261. return $list;
  262. }
  263. public static function breakItemGroup($list, $level = 0)
  264. {
  265. foreach ($list as $k => $v) {
  266. $shortCover = $v['cover'] ?? '';
  267. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 20;
  268. $stockInfo = self::formatStock($v['stock'], $ratio);
  269. $list[$k]['bigNum'] = $stockInfo['bigNum'];
  270. $list[$k]['smallNum'] = $stockInfo['smallNum'];
  271. $list[$k]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  272. }
  273. return $list;
  274. }
  275. public static function partItemGroup($list, $level = 0)
  276. {
  277. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  278. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  279. foreach ($list as $k => $v) {
  280. $shortCover = $v['cover'] ?? '';
  281. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 20;
  282. $stockInfo = self::formatStock($v['stock'], $ratio);
  283. $list[$k]['bigNum'] = $stockInfo['bigNum'];
  284. $list[$k]['smallNum'] = $stockInfo['smallNum'];
  285. $list[$k]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  286. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  287. $list[$k]['price'] = $price;
  288. $list[$k]['bigPrice'] = $price;
  289. $smallRatio = $v['smallRatio'] ?? 0;
  290. $smallPrice = bcdiv($price, $ratio, 2);
  291. $smallPrice = bcmul($smallPrice, $smallRatio, 2);
  292. $list[$k]['smallPrice'] = $smallPrice;
  293. }
  294. return $list;
  295. }
  296. public static function checkItemGroup($list, $level = 0)
  297. {
  298. foreach ($list as $k => $v) {
  299. $shortCover = $v['cover'] ?? '';
  300. $list[$k]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  301. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 1;
  302. $stockInfo = self::formatStock($v['stock'], $ratio);
  303. $list[$k]['bigNum'] = $stockInfo['bigNum'];
  304. $list[$k]['smallNum'] = $stockInfo['smallNum'];
  305. }
  306. return $list;
  307. }
  308. public static function cgItemGroup($list, $level = 0)
  309. {
  310. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  311. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  312. foreach ($list as $k => $v) {
  313. $shortCover = $v['cover'] ?? '';
  314. $list[$k]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  315. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 1;
  316. $stockInfo = self::formatStock($v['stock'], $ratio);
  317. $list[$k]['bigNum'] = $stockInfo['bigNum'];
  318. $list[$k]['smallNum'] = $stockInfo['smallNum'];
  319. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  320. $list[$k]['price'] = $price;
  321. $list[$k]['bigPrice'] = $price;
  322. $smallRatio = $v['smallRatio'] ?? 0;
  323. $smallPrice = bcdiv($price, $ratio, 2);
  324. $smallPrice = bcmul($smallPrice, $smallRatio, 2);
  325. $list[$k]['smallPrice'] = $smallPrice;
  326. }
  327. return $list;
  328. }
  329. //通用花材列表数组组合
  330. public static function changeStockGroup($list, $level = 0)
  331. {
  332. //各个等级对应的price addPrice字段
  333. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  334. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  335. foreach ($list as $k => $v) {
  336. $shortCover = $v['cover'] ?? '';
  337. $cover = imgUtil::getPrefix() . 'hhb_small.png';
  338. if (!empty($shortCover)) {
  339. $parse = parse_url(self::groupImg($shortCover));
  340. if (empty($parse['query'])) {
  341. $cover = self::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  342. } else {
  343. $cover = self::groupImg($shortCover);
  344. }
  345. }
  346. $list[$k]['cover'] = $cover;
  347. $list[$k]['prePrice'] = $v['price'] ?? 0;
  348. //今日特价、会员价
  349. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  350. //零售做特价时的原价
  351. if ($level == 0) {
  352. $currentAddPrice = $v['skMore'] ?? 0;
  353. $list[$k]['prePrice'] = bcadd($v['price'], $currentAddPrice, 2);
  354. }
  355. if (isset($v['discountPrice']) && $v['discountPrice'] > 0) {
  356. $currentAddPrice = $v['skMore'] ?? 0;
  357. $list[$k]['skPrice'] = bcadd($v['discountPrice'], $currentAddPrice, 2);
  358. }
  359. $list[$k]['price'] = $price;
  360. $cost = $v['cost'] ?? 0;
  361. $ml = 0;
  362. if ($price > 0) {
  363. $profit = bcsub($price, $cost, 2);
  364. $ml = $profit;
  365. $mll = bcdiv($profit, $price, 3);
  366. $mll = bcmul($mll, 100);
  367. $mll = round($mll, 1);
  368. } else {
  369. $mll = 0;
  370. }
  371. $list[$k]['mll'] = $mll;
  372. $list[$k]['ml'] = $ml;
  373. }
  374. return $list;
  375. }
  376. public static function stockOutGroup($list, $level = 0)
  377. {
  378. //各个等级对应的price addPrice字段
  379. $priceMap = \bizGhs\custom\classes\CustomClass::$levelPriceKeyMap;
  380. $addPriceMap = \bizGhs\custom\classes\CustomClass::$levelAddPriceKeyMap;
  381. foreach ($list as $k => $v) {
  382. $shortCover = $v['cover'] ?? '';
  383. $list[$k]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
  384. $price = \bizGhs\product\classes\ProductClass::getFinalPrice($v, $level, $priceMap, $addPriceMap);
  385. $list[$k]['price'] = $price;
  386. $list[$k]['bigPrice'] = $price;
  387. $smallRatio = $v['smallRatio'] ?? 0;
  388. $ratio = isset($v['ratio']) && $v['ratio'] > 0 ? $v['ratio'] : 20;
  389. $smallPrice = bcdiv($price, $ratio, 2);
  390. $smallPrice = bcmul($smallPrice, $smallRatio, 2);
  391. $list[$k]['smallPrice'] = $smallPrice;
  392. }
  393. return $list;
  394. }
  395. /**
  396. * 限购处理
  397. * @param $product
  398. * @param $customId 零售xhCustom表的id
  399. * @param $num
  400. */
  401. public static function handleLimitBuy($product, $customId, $num, $doCache = true)
  402. {
  403. $limitBuy = $product['limitBuy'];
  404. if ($limitBuy <= 0) {
  405. return;
  406. }
  407. $productId = $product['productId'];
  408. $limitKey = self::LIMIT_BUY_KEY . $productId;
  409. $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, $customId]);
  410. if (!empty($has)) {
  411. $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  412. self::recordLimitBuyRollbackSnapshot($productId, $customId, true, $hasNum);
  413. $lastNum = bcadd($hasNum, $num, 2);
  414. if ($lastNum > $limitBuy) {
  415. // 如果有特价,则超过的数量按原价卖;否则中断
  416. if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
  417. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
  418. return;
  419. }
  420. util::error(-1, '累计已超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$lastNum - $limitBuy]);
  421. }
  422. if ($doCache) {
  423. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
  424. }
  425. } else {
  426. self::recordLimitBuyRollbackSnapshot($productId, $customId, false, 0);
  427. if ($num > $limitBuy) {
  428. // 如果有特价,则超过的数量按原价卖;否则中断
  429. if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
  430. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
  431. return;
  432. }
  433. util::error(-1, '超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$num - $limitBuy]);
  434. }
  435. if ($doCache) {
  436. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
  437. }
  438. }
  439. }
  440. /**
  441. * 获取零售客户当前清单中各花材的限购情况(Redis 键为 hd_limit_buy:,与批发商 limit_buy_ 区分)
  442. * @param array $list 花材列表,项含 id 及 bigCount/smallCount 或 bigNum/smallNum
  443. * @param int|string $customId 零售端 xhCustom.id(花店客户)
  444. */
  445. public static function getLimitBuyInfoByList($list, $customId)
  446. {
  447. if (empty($list) || !is_array($list)) {
  448. return [];
  449. }
  450. $buyNumMap = [];
  451. $ids = [];
  452. foreach ($list as $item) {
  453. $productId = $item['id'] ?? 0;
  454. if (empty($productId)) {
  455. continue;
  456. }
  457. $productId = intval($productId);
  458. $bigNum = $item['bigCount'] ?? ($item['bigNum'] ?? 0);
  459. $smallNum = $item['smallCount'] ?? ($item['smallNum'] ?? 0);
  460. if (isset($buyNumMap[$productId]) == false) {
  461. $buyNumMap[$productId] = [
  462. 'bigNum' => 0,
  463. 'smallNum' => 0,
  464. ];
  465. }
  466. $buyNumMap[$productId]['bigNum'] = bcadd($buyNumMap[$productId]['bigNum'], $bigNum, 2);
  467. $buyNumMap[$productId]['smallNum'] = bcadd($buyNumMap[$productId]['smallNum'], $smallNum, 2);
  468. $ids[] = $productId;
  469. }
  470. $ids = array_values(array_unique($ids));
  471. if (empty($ids)) {
  472. return [];
  473. }
  474. $productData = \bizGhs\product\classes\ProductClass::getProductByIds($ids);
  475. $productData = array_column($productData, null, 'id');
  476. $respond = [];
  477. foreach ($ids as $productId) {
  478. $product = $productData[$productId] ?? [];
  479. if (empty($product)) {
  480. continue;
  481. }
  482. $limitBuy = $product['limitBuy'] ?? 0;
  483. $isLimit = $limitBuy > 0 ? true : false;
  484. $hasBuyNum = 0;
  485. if ($isLimit) {
  486. $limitKey = self::LIMIT_BUY_KEY . $productId;
  487. $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  488. $hasBuyNum = $hasBuyNum == null ? 0 : $hasBuyNum;
  489. }
  490. $buyNum = $buyNumMap[$productId] ?? [];
  491. $ratio = $product['ratio'] ?? 1;
  492. $currentBuyNum = \bizGhs\product\classes\ProductClass::mergeItemNum($buyNum['bigNum'] ?? 0, $buyNum['smallNum'] ?? 0, $ratio);
  493. $totalBuyNum = bcadd($hasBuyNum, $currentBuyNum, 2);
  494. $specialPrice = (
  495. ($product['hjDiscountPrice'] ?? 0) > 0
  496. && ($product['discountPrice'] ?? 0) > 0
  497. && ($product['skDiscountPrice'] ?? 0) > 0
  498. ) ? true : false;
  499. $respond[] = [
  500. 'id' => $productId,
  501. 'name' => $product['name'] ?? '',
  502. 'isLimit' => $isLimit,
  503. 'limitBuy' => (float)$limitBuy,
  504. 'specialPrice' => $specialPrice,
  505. 'hasBuyNum' => $isLimit ? (float)$hasBuyNum : 0,
  506. 'currentBuyNum' => (float)$currentBuyNum,
  507. 'reachLimitBuyNum' => ($isLimit && $totalBuyNum > $limitBuy) ? true : false,
  508. 'exceedNum' => $isLimit ? (float)bcsub($hasBuyNum, $limitBuy, 2) : 0,
  509. ];
  510. }
  511. return $respond;
  512. }
  513. //取消限购的缓存
  514. public static function cancelLimitBuy($productId)
  515. {
  516. $limitKey = self::LIMIT_BUY_KEY . $productId;
  517. $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
  518. if (!empty($arr)) {
  519. foreach ($arr as $field) {
  520. self::baseClearLimitBuy($productId, $field);
  521. }
  522. }
  523. self::clearLimitBuyClearMark($productId);
  524. self::clearLimitBuyClearLoopConfig($productId);
  525. }
  526. public static function getHasLimitBuyList($product)
  527. {
  528. $productId = $product->id;
  529. $limitBuy = $product->limitBuy ?? 0;
  530. $limitKey = self::LIMIT_BUY_KEY . $productId;
  531. $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
  532. $customList = [];
  533. if (!empty($arr)) {
  534. $numMap = [];
  535. $ids = [];
  536. foreach ($arr as $customId) {
  537. $num = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  538. $numMap[$customId] = $num;
  539. $ids[] = $customId;
  540. }
  541. $specialPrice = ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0); // 销售花材是否开启特价
  542. $customList = CustomClass::getAllByCondition(['id' => ['in', $ids]], null, '*');
  543. foreach ($customList as $key => $val) {
  544. $customId = $val['id'];
  545. $num = $numMap[$customId] ?? 0;
  546. $customList[$key]['hasLimitBuyNum'] = $num;
  547. $customList[$key]['reachLimitBuyNum'] = $num >= $limitBuy ? 1 : 0;
  548. $avatar = $val['avatar'] ?? '';
  549. $smallAvatar = imgUtil::groupImg($avatar);
  550. $customList[$key]['smallAvatar'] = $smallAvatar . "?x-oss-process=image/resize,m_fill,h_80,w_80";
  551. $customList[$key]['specialPrice'] = $specialPrice;
  552. }
  553. }
  554. return $customList;
  555. }
  556. /**
  557. * @param $productId
  558. * @param $customId
  559. * @param int $num -1 表示减全部,大于-1表示减值
  560. */
  561. public static function baseClearLimitBuy($productId, $customId, $num = -1)
  562. {
  563. $limitKey = self::LIMIT_BUY_KEY . $productId;
  564. if ($num <= -1) {
  565. //清掉全部
  566. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  567. } else {
  568. $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  569. $remainNum = $hasNum > $num ? bcsub($hasNum, $num) : 0;
  570. $remainNum = floor($remainNum);
  571. if ($remainNum > 0) {
  572. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $remainNum]);
  573. } else {
  574. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  575. }
  576. }
  577. }
  578. // 只清空已购买记录,不关闭商品限购配置
  579. public static function clearLimitBuyRecordByProductId($productId)
  580. {
  581. $productId = intval($productId);
  582. if ($productId <= 0) {
  583. return false;
  584. }
  585. $limitKey = self::LIMIT_BUY_KEY . $productId;
  586. Yii::$app->redis->executeCommand('DEL', [$limitKey]);
  587. return true;
  588. }
  589. // 清理限购清空标记
  590. public static function clearLimitBuyClearMark($productId)
  591. {
  592. $productId = intval($productId);
  593. if ($productId <= 0) {
  594. return false;
  595. }
  596. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  597. Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
  598. return true;
  599. }
  600. /**
  601. * 创建限购缓存(有过期时间)
  602. * @param $productId 花材ID
  603. * @param int $seconds 过期时间
  604. * @param array $options ['clearAt' => 0, 'recurring' => 0, 'intervalDays' => 0, 'clearHour' => 0] 循环清空配置
  605. * @return bool
  606. */
  607. public static function createLimitBuyCache($productId, $seconds = 0, $options = [])
  608. {
  609. $limitKey = self::LIMIT_BUY_KEY . $productId;
  610. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  611. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  612. $recurring = !empty($options['recurring']);
  613. $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, 0]);
  614. if (!empty($has)) {
  615. // 获取缓存超时时间
  616. $expire = Yii::$app->redis->executeCommand('TTL', [$limitKey]);
  617. if ($expire > 0) {
  618. //return;
  619. }
  620. //删除缓存
  621. Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
  622. }
  623. Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
  624. if ($seconds > 0) {
  625. $clearAt = intval($options['clearAt'] ?? 0);
  626. if ($clearAt <= 0) {
  627. $clearAt = time() + intval($seconds);
  628. }
  629. Yii::$app->redis->executeCommand('EXPIRE', [$limitKey, $seconds]);
  630. Yii::$app->redis->executeCommand('SET', [$clearMarkKey, $clearAt]);
  631. if ($recurring) {
  632. $intervalDays = intval($options['intervalDays'] ?? 0);
  633. $clearHour = intval($options['clearHour'] ?? -1);
  634. Yii::$app->redis->executeCommand('SET', [$clearLoopKey, json_encode([
  635. 'intervalDays' => $intervalDays,
  636. 'clearHour' => $clearHour,
  637. ], JSON_UNESCAPED_UNICODE)]);
  638. } else {
  639. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  640. }
  641. // 用 RabbitMQ 延迟消息在到期时清空订单项限购字段
  642. $message = [
  643. 'type' => 'limit_buy_clear',
  644. 'ptType' => 'hd',
  645. 'productId' => $productId,
  646. 'clearAt' => $clearAt,
  647. ];
  648. if ($recurring) {
  649. $message['recurring'] = 1;
  650. $message['intervalDays'] = intval($options['intervalDays'] ?? 0);
  651. $message['clearHour'] = intval($options['clearHour'] ?? -1);
  652. }
  653. $message = serialize($message);
  654. $producer = Yii::$app->rabbitmq->getProducer('stockProducer');
  655. $producer->publish($message, 'limitBuyDelayExchange', 'limitBuyDelayRoute', [
  656. 'delivery_mode' => 2,
  657. 'content_type' => 'application/octet-stream',
  658. 'application_headers' => new AMQPTable([
  659. 'x-delay' => intval($seconds * 1000),
  660. ]),
  661. ]);
  662. Yii::info('限购延迟消息已发送: ' . json_encode([
  663. 'type' => 'limit_buy_clear',
  664. 'ptType' => 'hd',
  665. 'productId' => intval($productId),
  666. 'clearAt' => intval($clearAt),
  667. 'delayMs' => intval($seconds * 1000),
  668. 'recurring' => $recurring ? 1 : 0,
  669. ], JSON_UNESCAPED_UNICODE), __METHOD__);
  670. } else {
  671. Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
  672. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  673. }
  674. }
  675. // 创建循环清空限购缓存
  676. public static function createRecurringLimitBuyCache($productId, $intervalDays, $clearHour, $clearAt = 0)
  677. {
  678. $productId = intval($productId);
  679. $intervalDays = intval($intervalDays);
  680. $clearHour = intval($clearHour);
  681. if ($productId <= 0 || $intervalDays <= 0 || $clearHour < 0 || $clearHour > 23) {
  682. return false;
  683. }
  684. if ($clearAt <= 0) {
  685. $clearAt = self::getNextLimitBuyClearAt($intervalDays, $clearHour);
  686. }
  687. $seconds = max(1, $clearAt - time());
  688. self::createLimitBuyCache($productId, $seconds, [
  689. 'recurring' => 1,
  690. 'intervalDays' => $intervalDays,
  691. 'clearHour' => $clearHour,
  692. 'clearAt' => $clearAt,
  693. ]);
  694. return true;
  695. }
  696. // 计算下一次循环清空时间
  697. public static function getNextLimitBuyClearAt($intervalDays, $clearHour, $baseTime = 0)
  698. {
  699. $baseTime = $baseTime > 0 ? intval($baseTime) : time();
  700. $targetDay = $baseTime + intval($intervalDays) * 86400;
  701. $clearAt = strtotime(date('Y-m-d', $targetDay) . ' ' . sprintf('%02d:00:00', intval($clearHour)));
  702. while ($clearAt <= $baseTime) {
  703. $clearAt = strtotime('+' . intval($intervalDays) . ' days', $clearAt);
  704. }
  705. return $clearAt;
  706. }
  707. // 获取循环清空限购配置
  708. public static function getLimitBuyClearLoopConfigByProductId($productId)
  709. {
  710. $productId = intval($productId);
  711. if ($productId <= 0) {
  712. return [];
  713. }
  714. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  715. $config = Yii::$app->redis->executeCommand('GET', [$clearLoopKey]);
  716. if (empty($config)) {
  717. return [];
  718. }
  719. $config = json_decode($config, true);
  720. return is_array($config) ? $config : [];
  721. }
  722. // 获取限购清空配置,用于商品详情回显
  723. public static function getLimitBuyClearInfo($productId)
  724. {
  725. $productId = intval($productId);
  726. $info = [
  727. 'limitBuyClearType' => 0, // 0不清空 1循环清空 2定时清空
  728. 'limitBuyClearTypeName' => '不清空',
  729. 'limitBuyClearAt' => 0,
  730. 'limitBuyClearTime' => '',
  731. 'limitBuyNextClearAt' => 0,
  732. 'limitBuyNextClearTime' => '',
  733. 'limitBuyClearIntervalDays' => 0,
  734. 'cleartAt' => '',
  735. ];
  736. if ($productId <= 0) {
  737. return $info;
  738. }
  739. $loopConfig = self::getLimitBuyClearLoopConfigByProductId($productId);
  740. if (!empty($loopConfig)) {
  741. $info['limitBuyClearType'] = 1;
  742. $info['limitBuyClearTypeName'] = '循环清空';
  743. $info['limitBuyClearIntervalDays'] = intval($loopConfig['intervalDays'] ?? 0);
  744. $info['cleartAt'] = intval($loopConfig['clearHour'] ?? 0);
  745. return $info;
  746. }
  747. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  748. $clearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
  749. if ($clearAt <= 0) {
  750. return $info;
  751. }
  752. $info['limitBuyClearType'] = 2;
  753. $info['limitBuyClearTypeName'] = '定时清空';
  754. $info['limitBuyClearAt'] = $clearAt;
  755. $info['limitBuyClearTime'] = date('Y-m-d H:i:s', $clearAt);
  756. $info['limitBuyNextClearAt'] = $clearAt;
  757. $info['limitBuyNextClearTime'] = $info['limitBuyClearTime'];
  758. return $info;
  759. }
  760. // 清理循环清空限购配置
  761. public static function clearLimitBuyClearLoopConfig($productId)
  762. {
  763. $productId = intval($productId);
  764. if ($productId <= 0) {
  765. return false;
  766. }
  767. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  768. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  769. return true;
  770. }
  771. /**
  772. * 记录限购缓存写入前快照(同一次请求内同商品同客户只记录一次)
  773. *
  774. * @param int $productId
  775. * @param int $customId 零售xhCustom表的id
  776. * @param bool $hasOldValue
  777. * @param string|int|float $oldValue
  778. * @return void
  779. */
  780. public static function recordLimitBuyRollbackSnapshot($productId, $customId, $hasOldValue, $oldValue = 0)
  781. {
  782. $productId = intval($productId);
  783. $customId = intval($customId);
  784. if ($productId <= 0 || $customId <= 0) {
  785. return;
  786. }
  787. $snapshotKey = $productId . '_' . $customId;
  788. if (!isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]) || !is_array(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY])) {
  789. Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] = [];
  790. }
  791. if (isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey])) {
  792. return;
  793. }
  794. Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey] = [
  795. 'productId' => $productId,
  796. 'customId' => $customId,
  797. 'hasOldValue' => $hasOldValue ? 1 : 0,
  798. 'oldValue' => strval($oldValue),
  799. ];
  800. }
  801. /**
  802. * 根据花材清空订单项限购值
  803. *
  804. * @param int $productId
  805. * @return bool
  806. */
  807. public static function clearLimitBuyByProductId($productId)
  808. {
  809. $productId = intval($productId);
  810. if ($productId <= 0) {
  811. return false;
  812. }
  813. self::updateById($productId, ['limitBuy' => 0]);
  814. return true;
  815. }
  816. /**
  817. * 回滚当前请求中已记录的限购缓存变更
  818. *
  819. * @return bool
  820. */
  821. public static function rollbackLimitBuySnapshot()
  822. {
  823. $snapshotList = Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] ?? [];
  824. if (empty($snapshotList) || !is_array($snapshotList)) {
  825. return true;
  826. }
  827. foreach ($snapshotList as $snapshot) {
  828. $productId = intval($snapshot['productId'] ?? 0);
  829. $customId = intval($snapshot['customId'] ?? 0);
  830. if ($productId <= 0 || $customId <= 0) {
  831. continue;
  832. }
  833. $limitKey = self::LIMIT_BUY_KEY . $productId;
  834. $hasOldValue = intval($snapshot['hasOldValue'] ?? 0);
  835. if ($hasOldValue == 1) {
  836. $oldValue = $snapshot['oldValue'] ?? '0';
  837. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $oldValue]);
  838. } else {
  839. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  840. }
  841. }
  842. self::clearLimitBuyRollbackSnapshot();
  843. return true;
  844. }
  845. /**
  846. * 清理当前请求中记录的限购回滚快照
  847. *
  848. * @return void
  849. */
  850. public static function clearLimitBuyRollbackSnapshot()
  851. {
  852. unset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]);
  853. }
  854. }