ProductClass.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  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 clearLimitBuyCache($productId, $clearAll = true)
  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. if ($clearAll === true) {
  524. self::clearLimitBuyClearMark($productId);
  525. self::clearLimitBuyClearLoopConfig($productId);
  526. }
  527. }
  528. public static function getHasLimitBuyList($product)
  529. {
  530. $productId = $product->id;
  531. $limitBuy = $product->limitBuy ?? 0;
  532. $limitKey = self::LIMIT_BUY_KEY . $productId;
  533. $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
  534. $customList = [];
  535. if (!empty($arr)) {
  536. $numMap = [];
  537. $ids = [];
  538. foreach ($arr as $customId) {
  539. $num = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  540. $numMap[$customId] = $num;
  541. $ids[] = $customId;
  542. }
  543. $specialPrice = ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0); // 销售花材是否开启特价
  544. $customList = CustomClass::getAllByCondition(['id' => ['in', $ids]], null, '*');
  545. foreach ($customList as $key => $val) {
  546. $customId = $val['id'];
  547. $num = $numMap[$customId] ?? 0;
  548. $customList[$key]['hasLimitBuyNum'] = $num;
  549. $customList[$key]['reachLimitBuyNum'] = $num >= $limitBuy ? 1 : 0;
  550. $avatar = $val['avatar'] ?? '';
  551. $smallAvatar = imgUtil::groupImg($avatar);
  552. $customList[$key]['smallAvatar'] = $smallAvatar . "?x-oss-process=image/resize,m_fill,h_80,w_80";
  553. $customList[$key]['specialPrice'] = $specialPrice;
  554. }
  555. }
  556. return $customList;
  557. }
  558. /**
  559. * @param $productId
  560. * @param $customId
  561. * @param int $num -1 表示减全部,大于-1表示减值
  562. */
  563. public static function baseClearLimitBuy($productId, $customId, $num = -1)
  564. {
  565. $limitKey = self::LIMIT_BUY_KEY . $productId;
  566. if ($num <= -1) {
  567. //清掉全部
  568. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  569. } else {
  570. $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
  571. $remainNum = $hasNum > $num ? bcsub($hasNum, $num) : 0;
  572. $remainNum = floor($remainNum);
  573. if ($remainNum > 0) {
  574. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $remainNum]);
  575. } else {
  576. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  577. }
  578. }
  579. }
  580. // 只清空已购买记录,不关闭商品限购配置
  581. public static function clearLimitBuyRecordByProductId($productId)
  582. {
  583. $productId = intval($productId);
  584. if ($productId <= 0) {
  585. return false;
  586. }
  587. $limitKey = self::LIMIT_BUY_KEY . $productId;
  588. Yii::$app->redis->executeCommand('DEL', [$limitKey]);
  589. return true;
  590. }
  591. // 校验清空限购消息是否是当前有效版本
  592. public static function checkLimitBuyClearMessage($productId, $clearAt)
  593. {
  594. $productId = intval($productId);
  595. $clearAt = intval($clearAt);
  596. if ($productId <= 0 || $clearAt <= 0) {
  597. return false;
  598. }
  599. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  600. $currentClearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
  601. return $currentClearAt > 0 && $currentClearAt == $clearAt;
  602. }
  603. // 清理限购清空标记
  604. public static function clearLimitBuyClearMark($productId)
  605. {
  606. $productId = intval($productId);
  607. if ($productId <= 0) {
  608. return false;
  609. }
  610. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  611. Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
  612. return true;
  613. }
  614. /**
  615. * 创建限购缓存(有过期时间)
  616. * @param $productId 花材ID
  617. * @param int $seconds 过期时间
  618. * @param array $options ['clearAt' => 0, 'recurring' => 0, 'intervalDays' => 0, 'clearHour' => 0] 循环清空配置
  619. * @return bool
  620. */
  621. public static function createLimitBuyCache($productId, $seconds = 0, $options = [])
  622. {
  623. $limitKey = self::LIMIT_BUY_KEY . $productId;
  624. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  625. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  626. $recurring = !empty($options['recurring']);
  627. $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, 0]);
  628. if (!empty($has)) {
  629. // 获取缓存超时时间
  630. $expire = Yii::$app->redis->executeCommand('TTL', [$limitKey]);
  631. if ($expire > 0) {
  632. //return;
  633. }
  634. //删除缓存
  635. Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
  636. }
  637. Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
  638. if ($seconds > 0) {
  639. $clearAt = intval($options['clearAt'] ?? 0);
  640. if ($clearAt <= 0) {
  641. $clearAt = time() + intval($seconds);
  642. }
  643. Yii::$app->redis->executeCommand('EXPIRE', [$limitKey, $seconds]);
  644. Yii::$app->redis->executeCommand('SET', [$clearMarkKey, $clearAt]);
  645. if ($recurring) {
  646. $intervalDays = intval($options['intervalDays'] ?? 0);
  647. $clearHour = intval($options['clearHour'] ?? -1);
  648. Yii::$app->redis->executeCommand('SET', [$clearLoopKey, json_encode([
  649. 'intervalDays' => $intervalDays,
  650. 'clearHour' => $clearHour,
  651. ], JSON_UNESCAPED_UNICODE)]);
  652. } else {
  653. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  654. }
  655. // 用 RabbitMQ 延迟消息在到期时清空订单项限购字段
  656. $message = [
  657. 'type' => 'limit_buy_clear',
  658. 'ptType' => 'hd',
  659. 'productId' => $productId,
  660. 'clearAt' => $clearAt,
  661. ];
  662. if ($recurring) {
  663. $message['recurring'] = 1;
  664. $message['intervalDays'] = intval($options['intervalDays'] ?? 0);
  665. $message['clearHour'] = intval($options['clearHour'] ?? -1);
  666. }
  667. $message = serialize($message);
  668. $producer = Yii::$app->rabbitmq->getProducer('stockProducer');
  669. $producer->publish($message, 'limitBuyDelayExchange', 'limitBuyDelayRoute', [
  670. 'delivery_mode' => 2,
  671. 'content_type' => 'application/octet-stream',
  672. 'application_headers' => new AMQPTable([
  673. 'x-delay' => intval($seconds * 1000),
  674. ]),
  675. ]);
  676. Yii::info('限购延迟消息已发送: ' . json_encode([
  677. 'type' => 'limit_buy_clear',
  678. 'ptType' => 'hd',
  679. 'productId' => intval($productId),
  680. 'clearAt' => intval($clearAt),
  681. 'delayMs' => intval($seconds * 1000),
  682. 'recurring' => $recurring ? 1 : 0,
  683. ], JSON_UNESCAPED_UNICODE), __METHOD__);
  684. } else {
  685. Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
  686. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  687. }
  688. }
  689. // 创建循环清空限购缓存
  690. public static function createRecurringLimitBuyCache($productId, $intervalDays, $clearHour, $clearAt = 0)
  691. {
  692. $productId = intval($productId);
  693. $intervalDays = intval($intervalDays);
  694. $clearHour = intval($clearHour);
  695. if ($productId <= 0 || $intervalDays <= 0 || $clearHour < 0 || $clearHour > 23) {
  696. return false;
  697. }
  698. if ($clearAt <= 0) {
  699. $clearAt = self::getNextLimitBuyClearAt($intervalDays, $clearHour);
  700. }
  701. $seconds = max(1, $clearAt - time());
  702. self::createLimitBuyCache($productId, $seconds, [
  703. 'recurring' => 1,
  704. 'intervalDays' => $intervalDays,
  705. 'clearHour' => $clearHour,
  706. 'clearAt' => $clearAt,
  707. ]);
  708. return true;
  709. }
  710. // 计算下一次循环清空时间
  711. public static function getNextLimitBuyClearAt($intervalDays, $clearHour, $baseTime = 0)
  712. {
  713. $baseTime = $baseTime > 0 ? intval($baseTime) : time();
  714. $targetDay = $baseTime + intval($intervalDays) * 86400;
  715. $clearAt = strtotime(date('Y-m-d', $targetDay) . ' ' . sprintf('%02d:00:00', intval($clearHour)));
  716. while ($clearAt <= $baseTime) {
  717. $clearAt = strtotime('+' . intval($intervalDays) . ' days', $clearAt);
  718. }
  719. return $clearAt;
  720. }
  721. // 获取循环清空限购配置
  722. public static function getLimitBuyClearLoopConfigByProductId($productId)
  723. {
  724. $productId = intval($productId);
  725. if ($productId <= 0) {
  726. return [];
  727. }
  728. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  729. $config = Yii::$app->redis->executeCommand('GET', [$clearLoopKey]);
  730. if (empty($config)) {
  731. return [];
  732. }
  733. $config = json_decode($config, true);
  734. return is_array($config) ? $config : [];
  735. }
  736. // 获取限购清空配置,用于商品详情回显
  737. public static function getLimitBuyClearInfo($productId)
  738. {
  739. $productId = intval($productId);
  740. $info = [
  741. 'limitBuyClearType' => 0, // 0不清空 1循环清空 2定时清空
  742. 'limitBuyClearTypeName' => '不清空',
  743. 'limitBuyClearAt' => 0,
  744. 'limitBuyClearTime' => '',
  745. 'limitBuyNextClearAt' => 0,
  746. 'limitBuyNextClearTime' => '',
  747. 'limitBuyClearIntervalDays' => 0,
  748. 'cleartAt' => '',
  749. ];
  750. if ($productId <= 0) {
  751. return $info;
  752. }
  753. $loopConfig = self::getLimitBuyClearLoopConfigByProductId($productId);
  754. if (!empty($loopConfig)) {
  755. $info['limitBuyClearType'] = 1;
  756. $info['limitBuyClearTypeName'] = '循环清空';
  757. $info['limitBuyClearIntervalDays'] = intval($loopConfig['intervalDays'] ?? 0);
  758. $info['cleartAt'] = intval($loopConfig['clearHour'] ?? 0);
  759. return $info;
  760. }
  761. $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
  762. $clearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
  763. if ($clearAt <= 0) {
  764. return $info;
  765. }
  766. $info['limitBuyClearType'] = 2;
  767. $info['limitBuyClearTypeName'] = '定时清空';
  768. $info['limitBuyClearAt'] = $clearAt;
  769. $info['limitBuyClearTime'] = date('Y-m-d H:i:s', $clearAt);
  770. $info['limitBuyNextClearAt'] = $clearAt;
  771. $info['limitBuyNextClearTime'] = $info['limitBuyClearTime'];
  772. return $info;
  773. }
  774. // 清理循环清空限购配置
  775. public static function clearLimitBuyClearLoopConfig($productId)
  776. {
  777. $productId = intval($productId);
  778. if ($productId <= 0) {
  779. return false;
  780. }
  781. $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
  782. Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
  783. return true;
  784. }
  785. /**
  786. * 记录限购缓存写入前快照(同一次请求内同商品同客户只记录一次)
  787. *
  788. * @param int $productId
  789. * @param int $customId 零售xhCustom表的id
  790. * @param bool $hasOldValue
  791. * @param string|int|float $oldValue
  792. * @return void
  793. */
  794. public static function recordLimitBuyRollbackSnapshot($productId, $customId, $hasOldValue, $oldValue = 0)
  795. {
  796. $productId = intval($productId);
  797. $customId = intval($customId);
  798. if ($productId <= 0 || $customId <= 0) {
  799. return;
  800. }
  801. $snapshotKey = $productId . '_' . $customId;
  802. if (!isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]) || !is_array(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY])) {
  803. Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] = [];
  804. }
  805. if (isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey])) {
  806. return;
  807. }
  808. Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey] = [
  809. 'productId' => $productId,
  810. 'customId' => $customId,
  811. 'hasOldValue' => $hasOldValue ? 1 : 0,
  812. 'oldValue' => strval($oldValue),
  813. ];
  814. }
  815. /**
  816. * 根据花材清空订单项限购值
  817. *
  818. * @param int $productId
  819. * @return bool
  820. */
  821. public static function clearLimitBuyByProductId($productId)
  822. {
  823. $productId = intval($productId);
  824. if ($productId <= 0) {
  825. return false;
  826. }
  827. self::updateById($productId, ['limitBuy' => 0]);
  828. return true;
  829. }
  830. /**
  831. * 回滚当前请求中已记录的限购缓存变更
  832. *
  833. * @return bool
  834. */
  835. public static function rollbackLimitBuySnapshot()
  836. {
  837. $snapshotList = Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] ?? [];
  838. if (empty($snapshotList) || !is_array($snapshotList)) {
  839. return true;
  840. }
  841. foreach ($snapshotList as $snapshot) {
  842. $productId = intval($snapshot['productId'] ?? 0);
  843. $customId = intval($snapshot['customId'] ?? 0);
  844. if ($productId <= 0 || $customId <= 0) {
  845. continue;
  846. }
  847. $limitKey = self::LIMIT_BUY_KEY . $productId;
  848. $hasOldValue = intval($snapshot['hasOldValue'] ?? 0);
  849. if ($hasOldValue == 1) {
  850. $oldValue = $snapshot['oldValue'] ?? '0';
  851. Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $oldValue]);
  852. } else {
  853. Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
  854. }
  855. }
  856. self::clearLimitBuyRollbackSnapshot();
  857. return true;
  858. }
  859. /**
  860. * 清理当前请求中记录的限购回滚快照
  861. *
  862. * @return void
  863. */
  864. public static function clearLimitBuyRollbackSnapshot()
  865. {
  866. unset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]);
  867. }
  868. }