StoreSeckillServices.php 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\services\activity\seckill;
  13. use app\Request;
  14. use app\services\activity\StoreActivityServices;
  15. use app\services\BaseServices;
  16. use app\dao\activity\seckill\StoreSeckillDao;
  17. use app\services\diy\DiyServices;
  18. use app\services\order\StoreOrderServices;
  19. use app\services\other\QrcodeServices;
  20. use app\services\product\branch\StoreBranchProductServices;
  21. use app\services\product\ensure\StoreProductEnsureServices;
  22. use app\services\product\label\StoreProductLabelServices;
  23. use app\services\product\product\StoreDescriptionServices;
  24. use app\services\store\SystemStoreServices;
  25. use app\services\user\UserRelationServices;
  26. use app\services\product\product\StoreProductReplyServices;
  27. use app\services\product\product\StoreProductServices;
  28. use app\services\product\sku\StoreProductAttrResultServices;
  29. use app\services\product\sku\StoreProductAttrServices;
  30. use app\services\product\sku\StoreProductAttrValueServices;
  31. use app\jobs\product\ProductLogJob;
  32. use crmeb\exceptions\AdminException;
  33. use crmeb\services\CacheService;
  34. use crmeb\services\SystemConfigService;
  35. use think\exception\ValidateException;
  36. /**
  37. * Class StoreSeckillServices
  38. * @package app\services\activity\seckill
  39. * @method getSeckillIdsArray(array $ids, array $field)
  40. * @mixin StoreSeckillDao
  41. */
  42. class StoreSeckillServices extends BaseServices
  43. {
  44. const OPOXMWTJ = 'k8kkOJ';
  45. /**
  46. * 商品活动类型
  47. */
  48. const TYPE = 1;
  49. /**
  50. * StoreSeckillServices constructor.
  51. * @param StoreSeckillDao $dao
  52. */
  53. public function __construct(StoreSeckillDao $dao)
  54. {
  55. $this->dao = $dao;
  56. }
  57. /**
  58. * @param array $productIds
  59. * @return array
  60. * @author 等风来
  61. * @email 136327134@qq.com
  62. * @date 2022/11/14
  63. */
  64. public function getSeckillIdsArrayCache(array $productIds)
  65. {
  66. $list = $this->dao->cacheList();
  67. if (!$list) {
  68. return $this->dao->getSeckillIdsArray($productIds, ['id', 'time_id', 'product_id']);
  69. } else {
  70. $seckill = [];
  71. $time = time();
  72. foreach ($list as $item) {
  73. if ($item['is_del'] == 0 && $item['status'] == 1 && $item['start_time'] <= $time && $item['stop_time'] >= ($time - 86400) && in_array($item['product_id'], $productIds)) {
  74. $seckill[] = [
  75. 'id' => $item['id'],
  76. 'time_id' => $item['time_id'],
  77. 'product_id' => $item['product_id'],
  78. ];
  79. }
  80. }
  81. return $seckill;
  82. }
  83. }
  84. public function getCount(array $where)
  85. {
  86. $this->dao->count($where);
  87. }
  88. /**
  89. * 秒杀是否存在
  90. * @param int $id
  91. * @param string $field
  92. * @return array|int|\think\Model
  93. * @throws \think\db\exception\DataNotFoundException
  94. * @throws \think\db\exception\DbException
  95. * @throws \think\db\exception\ModelNotFoundException
  96. */
  97. public function getSeckillCount(int $id = 0, string $field = 'time_id')
  98. {
  99. $where = [];
  100. $where[] = ['is_del', '=', 0];
  101. $where[] = ['status', '=', 1];$config = [];
  102. $currentHour = date('Hi');
  103. /** @var StoreSeckillTimeServices $storeSeckillTimeServices */
  104. $storeSeckillTimeServices = app()->make(StoreSeckillTimeServices::class);
  105. if ($id) {
  106. $time = time();
  107. $where[] = ['id', '=', $id];
  108. $where[] = ['start_time', '<=', $time];
  109. $where[] = ['stop_time', '>=', $time - 86400];
  110. $seckill_one = $this->dao->getOne($where, $field);
  111. if (!$seckill_one) {
  112. throw new ValidateException('活动已结束');
  113. }
  114. $time_id = $seckill_one['time_id'] ?? [];
  115. if ($time_id) {
  116. $time_id = is_string($time_id) ? explode(',', $time_id) : $time_id;
  117. $timeList = $storeSeckillTimeServices->getList(['id' => $time_id, 'status' => 1]);
  118. foreach ($timeList as $value) {
  119. $value['start_time'] = $start = str_replace(':', '', $value['start_time']);
  120. $value['end_time'] = $end = str_replace(':', '', $value['end_time']);
  121. if ($currentHour >= $start && $currentHour < $end) {
  122. $config = $value;
  123. break;
  124. }
  125. }
  126. }
  127. if (!$config) {
  128. throw new ValidateException('活动已结束');
  129. }
  130. //获取秒杀商品状态
  131. if ($config['start_time'] <= $currentHour && $config['end_time'] > $currentHour) {
  132. return $seckill_one;
  133. } else if ($config['start_time'] > $currentHour) {
  134. throw new ValidateException('活动未开始');
  135. } else {
  136. throw new ValidateException('活动已结束');
  137. }
  138. } else {
  139. $timeList = $storeSeckillTimeServices->getList(['status' => 1]);
  140. foreach ($timeList as $value) {
  141. $start = str_replace(':', '', $value['start_time']);
  142. $end = str_replace(':', '', $value['end_time']);
  143. if ($currentHour >= $start && $currentHour < $end) {
  144. $config = $value;
  145. break;
  146. }
  147. }
  148. if (!$config) return 0;
  149. //获取秒杀商品状态
  150. $start = substr_replace($config['start_time'], ':', 2, 0);
  151. $end = substr_replace($config['end_time'], ':', 2, 0);
  152. $startTime = strtotime(date('Y-m-d') . ' '. $start);
  153. $stopTime = strtotime(date('Y-m-d') . ' '. $end);
  154. $where[] = ['start_time', '<', $startTime];
  155. $where[] = ['stop_time', '>', $stopTime];
  156. return $this->dao->getCount($where);
  157. }
  158. }
  159. /**
  160. * 保存数据
  161. * @param int $id
  162. * @param array $data
  163. */
  164. public function saveData(int $id, array $data)
  165. {
  166. /** @var StoreProductServices $storeProductServices */
  167. $storeProductServices = app()->make(StoreProductServices::class);
  168. $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']]);
  169. if (!$productInfo) {
  170. throw new AdminException('原商品已下架或移入回收站');
  171. }
  172. if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) {
  173. throw new AdminException('该商品是预售或svip专享');
  174. }
  175. $data['product_type'] = $productInfo['product_type'];
  176. $data['type'] = $productInfo['type'] ?? 0;
  177. $data['relation_id'] = $productInfo['relation_id'] ?? 0;
  178. $custom_form = $productInfo['custom_form'] ?? [];
  179. $data['custom_form'] = is_array($custom_form) ? json_encode($custom_form) : $custom_form;
  180. $data['system_form_id'] = $productInfo['system_form_id'] ?? 0;
  181. $store_label_id = $productInfo['store_label_id'] ?? [];
  182. $data['store_label_id'] = is_array($store_label_id) ? implode(',', $store_label_id) : $store_label_id;
  183. $ensure_id = $productInfo['ensure_id'] ?? [];
  184. $data['ensure_id'] = is_array($ensure_id) ? implode(',', $ensure_id) : $ensure_id;
  185. $specs = $productInfo['specs'] ?? [];
  186. $data['specs'] = is_array($specs) ? json_encode($specs) : $specs;
  187. if (in_array($data['product_type'], [1, 2, 3])) {
  188. $data['freight'] = 2;
  189. $data['temp_id'] = 0;
  190. $data['postage'] = 0;
  191. } else {
  192. if ($data['freight'] == 1) {
  193. $data['temp_id'] = 0;
  194. $data['postage'] = 0;
  195. } elseif ($data['freight'] == 2) {
  196. $data['temp_id'] = 0;
  197. } elseif ($data['freight'] == 3) {
  198. $data['postage'] = 0;
  199. }
  200. if ($data['freight'] == 2 && !$data['postage']) {
  201. throw new AdminException('请设置运费金额');
  202. }
  203. if ($data['freight'] == 3 && !$data['temp_id']) {
  204. throw new AdminException('请选择运费模版');
  205. }
  206. }
  207. $description = $data['description'];
  208. $detail = $data['attrs'];
  209. $items = $data['items'];
  210. $data['start_time'] = strtotime($data['section_time'][0]);
  211. $data['stop_time'] = strtotime($data['section_time'][1]);
  212. $data['image'] = $data['images'][0] ?? '';
  213. $data['images'] = json_encode($data['images']);
  214. $data['price'] = min(array_column($detail, 'price'));
  215. $data['ot_price'] = min(array_column($detail, 'ot_price'));
  216. $data['quota'] = $data['quota_show'] = array_sum(array_column($detail, 'quota'));
  217. if ($data['quota'] > $productInfo['stock']) {
  218. throw new ValidateException('限量不能超过商品库存');
  219. }
  220. $data['stock'] = array_sum(array_column($detail, 'stock'));
  221. unset($data['section_time'], $data['description'], $data['attrs'], $data['items']);
  222. /** @var StoreDescriptionServices $storeDescriptionServices */
  223. $storeDescriptionServices = app()->make(StoreDescriptionServices::class);
  224. /** @var StoreProductAttrServices $storeProductAttrServices */
  225. $storeProductAttrServices = app()->make(StoreProductAttrServices::class);
  226. $id = $this->transaction(function () use ($id, $data, $description, $detail, $items, $storeDescriptionServices, $storeProductAttrServices) {
  227. if ($id) {
  228. $res = $this->dao->update($id, $data);
  229. if (!$res) throw new AdminException('修改失败');
  230. } else {
  231. $data['add_time'] = time();
  232. $res = $this->dao->save($data);
  233. if (!$res) throw new AdminException('添加失败');
  234. $id = (int)$res->id;
  235. }
  236. $storeDescriptionServices->saveDescription((int)$id, $description, 1);
  237. $skuList = $storeProductAttrServices->validateProductAttr($items, $detail, (int)$id, 1);
  238. $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$id, 1);
  239. $res = true;
  240. foreach ($valueGroup as $item) {
  241. $res = $res && CacheService::setStock($item['unique'], (int)$item['quota_show']);
  242. }
  243. if (!$res) {
  244. throw new AdminException('占用库存失败');
  245. }
  246. return $id;
  247. });
  248. $this->dao->cacheTag()->clear();
  249. //保存
  250. $seckill = $this->dao->get($id, ['*'], ['descriptions', 'activity' => function ($query) {
  251. $query->field('id,start_day,end_day,time_id');
  252. }]);
  253. $this->dao->cacheUpdate($seckill->toArray());
  254. CacheService::redisHandler('product_attr')->clear();
  255. }
  256. /**
  257. * 获取列表
  258. * @param array $where
  259. * @return array
  260. * @throws \think\db\exception\DataNotFoundException
  261. * @throws \think\db\exception\DbException
  262. * @throws \think\db\exception\ModelNotFoundException
  263. */
  264. public function systemPage(array $where)
  265. {
  266. [$page, $limit] = $this->getPageValue();
  267. $list = $this->dao->getList($where, $page, $limit, ['activityName']);
  268. $count = $this->dao->count($where);
  269. foreach ($list as &$item) {
  270. $item['store_name'] = $item['title'];
  271. if ($item['status']) {
  272. if ($item['start_time'] > time())
  273. $item['start_name'] = '未开始';
  274. else if (bcadd($item['stop_time'], '86400') < time())
  275. $item['start_name'] = '已结束';
  276. else if (bcadd($item['stop_time'], '86400') > time() && $item['start_time'] < time()) {
  277. $item['start_name'] = '进行中';
  278. }
  279. } else $item['start_name'] = '已结束';
  280. $end_time = $item['stop_time'] ? (date('Y-m-d', (int)$item['stop_time']) . ' 23:59:59') : '';
  281. $item['_stop_time'] = $end_time;
  282. $item['stop_status'] = $item['stop_time'] + 86400 < time() ? 1 : 0;
  283. }
  284. return compact('list', 'count');
  285. }
  286. /**
  287. * 获取秒杀详情
  288. * @param int $id
  289. * @return array|\think\Model|null
  290. */
  291. public function getInfo(int $id)
  292. {
  293. $info = $this->dao->get($id);
  294. if ($info) {
  295. if ($info['start_time'])
  296. $start_time = date('Y-m-d', (int)$info['start_time']);
  297. if ($info['stop_time'])
  298. $stop_time = date('Y-m-d', (int)$info['stop_time']);
  299. if (isset($start_time) && isset($stop_time))
  300. $info['section_time'] = [$start_time, $stop_time];
  301. else
  302. $info['section_time'] = [];
  303. unset($info['start_time'], $info['stop_time']);
  304. $info['give_integral'] = intval($info['give_integral']);
  305. $info['price'] = floatval($info['price']);
  306. $info['ot_price'] = floatval($info['ot_price']);
  307. $info['postage'] = floatval($info['postage']);
  308. $info['cost'] = floatval($info['cost']);
  309. $info['weight'] = floatval($info['weight']);
  310. $info['volume'] = floatval($info['volume']);
  311. if (!$info['delivery_type']) {
  312. $info['delivery_type'] = [1];
  313. }
  314. if ($info['postage']) {
  315. $info['freight'] = 2;
  316. } elseif ($info['temp_id']) {
  317. $info['freight'] = 3;
  318. } else {
  319. $info['freight'] = 1;
  320. }
  321. /** @var StoreDescriptionServices $storeDescriptionServices */
  322. $storeDescriptionServices = app()->make(StoreDescriptionServices::class);
  323. $info['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 1]);
  324. $info['attrs'] = $this->attrList($id, $info['product_id']);
  325. //适用门店
  326. $info['stores'] = [];
  327. if (isset($info['applicable_type']) && ($info['applicable_type'] == 1 || ($info['applicable_type'] == 2 && isset($info['applicable_store_id']) && $info['applicable_store_id']))) {//查询门店信息
  328. $where = ['is_del' => 0];
  329. if ($info['applicable_type'] == 2) {
  330. $store_ids = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']);
  331. $where['id'] = $store_ids;
  332. }
  333. $field = ['id', 'cate_id', 'name', 'phone', 'address', 'detailed_address', 'image', 'is_show', 'day_time', 'day_start', 'day_end'];
  334. /** @var SystemStoreServices $storeServices */
  335. $storeServices = app()->make(SystemStoreServices::class);
  336. $storeData = $storeServices->getStoreList($where, $field, '', '', 0, ['categoryName']);
  337. $info['stores'] = $storeData['list'] ?? [];
  338. }
  339. }
  340. return $info;
  341. }
  342. /**
  343. * 获取规格
  344. * @param int $id
  345. * @param int $pid
  346. * @return mixed
  347. */
  348. public function attrList(int $id, int $pid)
  349. {
  350. /** @var StoreProductAttrResultServices $storeProductAttrResultServices */
  351. $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class);
  352. $seckillResult = $storeProductAttrResultServices->value(['product_id' => $id, 'type' => 1], 'result');
  353. $items = json_decode($seckillResult, true)['attr'];
  354. $productAttr = $this->getAttr($items, $pid, 0);
  355. $seckillAttr = $this->getAttr($items, $id, 1);
  356. foreach ($productAttr as $pk => $pv) {
  357. foreach ($seckillAttr as &$sv) {
  358. if ($pv['detail'] == $sv['detail']) {
  359. $productAttr[$pk] = $sv;
  360. }
  361. }
  362. $productAttr[$pk]['detail'] = json_decode($productAttr[$pk]['detail']);
  363. }
  364. $attrs['items'] = $items;
  365. $attrs['value'] = $productAttr;
  366. foreach ($items as $key => $item) {
  367. $header[] = ['title' => $item['value'], 'key' => 'value' . ($key + 1), 'align' => 'center', 'minWidth' => 80];
  368. }
  369. $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120];
  370. $header[] = ['title' => '秒杀价', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80];
  371. $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80];
  372. $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80];
  373. $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80];
  374. $header[] = ['title' => '限量', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80];
  375. $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80];
  376. $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80];
  377. $header[] = ['title' => '商品条形码', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80];
  378. $header[] = ['title' => '商品编号', 'key' => 'code', 'align' => 'center', 'minWidth' => 80];
  379. $attrs['header'] = $header;
  380. return $attrs;
  381. }
  382. /**
  383. * 获取规格
  384. * @param $attr
  385. * @param $id
  386. * @param $type
  387. * @return array
  388. */
  389. public function getAttr($attr, $id, $type)
  390. {
  391. /** @var StoreProductAttrValueServices $storeProductAttrValueServices */
  392. $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
  393. $value = attr_format($attr)[1];
  394. $valueNew = [];
  395. $count = 0;
  396. foreach ($value as $key => $item) {
  397. $detail = $item['detail'];
  398. // sort($item['detail'], SORT_STRING);
  399. $suk = implode(',', $item['detail']);
  400. $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk');
  401. if (count($sukValue)) {
  402. foreach (array_values($detail) as $k => $v) {
  403. $valueNew[$count]['value' . ($k + 1)] = $v;
  404. }
  405. $valueNew[$count]['detail'] = json_encode($detail);
  406. $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? '';
  407. $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0;
  408. $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0;
  409. $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0;
  410. $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0;
  411. // $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0;
  412. $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0;
  413. $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? '';
  414. $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? '';
  415. $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0;
  416. $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0;
  417. $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0;
  418. $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0;
  419. $valueNew[$count]['_checked'] = $type != 0;
  420. $count++;
  421. }
  422. }
  423. return $valueNew;
  424. }
  425. /**
  426. * 获取某个时间段的秒杀列表
  427. * @param int $time
  428. * @return array
  429. * @throws \think\db\exception\DataNotFoundException
  430. * @throws \think\db\exception\DbException
  431. * @throws \think\db\exception\ModelNotFoundException
  432. */
  433. public function getListByTime(int $time, array $ids = [], bool $isStore = false)
  434. {
  435. [$page, $limit] = $this->getPageValue();
  436. /** @var StoreActivityServices $storeActivityServices */
  437. $storeActivityServices = app()->make(StoreActivityServices::class);
  438. $activityList = $storeActivityServices->getList(['time_id' => $time, 'type' => 1, 'status' => 1, 'is_del' => 0, 'activityTime' => true], 'id,image');
  439. $seckillInfo = [];
  440. if ($activityList) {
  441. $activityIds = array_column($activityList, 'id');
  442. $seckillInfo = $this->dao->getListByTime($activityIds, $ids, $page, $limit, $isStore);
  443. if (count($seckillInfo)) {
  444. $activityList = array_combine($activityIds, $activityList);
  445. foreach ($seckillInfo as $key => &$item) {
  446. if ($item['quota'] > 0) {
  447. $percent = (float)sprintf("%.1f", (($item['quota_show'] - $item['quota']) / $item['quota_show'] * 100));
  448. $item['percent'] = $percent;
  449. $item['stock'] = $item['quota'];
  450. } else {
  451. $item['percent'] = 100;
  452. $item['stock'] = 0;
  453. }
  454. $item['price'] = floatval($item['price']);
  455. $item['ot_price'] = floatval($item['ot_price']);
  456. $item['discount_num'] = $item['ot_price'] ? (float)bcmul(bcdiv((string)$item['price'], (string)$item['ot_price'], 4), '10', 1) : 10;
  457. $item['activity_image'] = $activityList[$item['activity_id']]['image'] ?? '';
  458. }
  459. }
  460. }
  461. return $seckillInfo;
  462. }
  463. /**
  464. * 获取某个时间段的秒杀商品数量
  465. * @param int $time
  466. * @param array $ids
  467. * @param array $not_ids
  468. * @param bool $isStore
  469. * @return int
  470. * @throws \think\db\exception\DataNotFoundException
  471. * @throws \think\db\exception\DbException
  472. * @throws \think\db\exception\ModelNotFoundException
  473. */
  474. public function getCountByTime(int $time, array $ids = [], array $not_ids = [], bool $isStore = false)
  475. {
  476. /** @var StoreActivityServices $storeActivityServices */
  477. $storeActivityServices = app()->make(StoreActivityServices::class);
  478. $activityList = $storeActivityServices->getList(['time_id' => $time, 'type' => 1, 'status' => 1, 'is_del' => 0, 'activityTime' => true], 'id');
  479. $count = 0;
  480. if ($activityList) {
  481. $activityIds = array_column($activityList, 'id');
  482. $count = $this->dao->getTimeCount($activityIds, $ids, $not_ids, $isStore);
  483. }
  484. return $count;
  485. }
  486. /**
  487. * 获取秒杀详情
  488. * @param Request $request
  489. * @param int $id
  490. * @return mixed
  491. * @throws \think\db\exception\DataNotFoundException
  492. * @throws \think\db\exception\DbException
  493. * @throws \think\db\exception\ModelNotFoundException
  494. */
  495. public function seckillDetail(Request $request, int $id)
  496. {
  497. $uid = (int)$request->uid();
  498. //读取秒杀商品缓存信息
  499. $storeInfo = $this->dao->cacheRemember($id, function () use ($id) {
  500. $storeInfo = $this->dao->getOne(['id' => $id], '*', ['descriptions']);
  501. if (!$storeInfo) {
  502. throw new ValidateException('商品不存在');
  503. } else {
  504. $storeInfo = $storeInfo->toArray();
  505. }
  506. return $storeInfo;
  507. });
  508. $storeInfo['activity'] = [];
  509. if ($storeInfo['activity_id']) {
  510. $activityServices = app()->make(StoreActivityServices::class);
  511. $storeInfo['activity'] = $activityServices->getInfo((int)$storeInfo['activity_id'], ['id', 'start_day', 'end_day', 'time_id']);
  512. }
  513. /** @var DiyServices $diyServices */
  514. $diyServices = app()->make(DiyServices::class);
  515. $infoDiy = $diyServices->getProductDetailDiy();
  516. //diy控制参数
  517. if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) {
  518. $storeInfo['specs'] = [];
  519. }
  520. $configData = SystemConfigService::more(['site_url', 'routine_contact_type', 'site_name', 'share_qrcode', 'store_self_mention', 'store_func_status', 'product_poster_title']);
  521. $siteUrl = $configData['site_url'] ?? '';
  522. $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl);
  523. $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl);
  524. //品牌名称
  525. /** @var StoreProductServices $storeProductServices */
  526. $storeProductServices = app()->make(StoreProductServices::class);
  527. $productInfo = $storeProductServices->getCacheProductInfo((int)$storeInfo['product_id']);
  528. $storeInfo['brand_name'] = $storeProductServices->productIdByBrandName($storeInfo['product_id'], $productInfo);
  529. $delivery_type = $storeInfo['delivery_type'] ?? $productInfo['delivery_type'];
  530. $storeInfo['delivery_type'] = is_string($delivery_type) ? explode(',', $delivery_type) : $delivery_type;
  531. /**
  532. * 判断配送方式
  533. */
  534. $storeInfo['delivery_type'] = $storeProductServices->getDeliveryType((int)$storeInfo['type'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'], $storeInfo['delivery_type']);
  535. $storeInfo['total'] = $productInfo['sales'] + $productInfo['ficti'];
  536. $storeInfo['store_label'] = $storeInfo['ensure'] = [];
  537. if ($storeInfo['store_label_id']) {
  538. /** @var StoreProductLabelServices $storeProductLabelServices */
  539. $storeProductLabelServices = app()->make(StoreProductLabelServices::class);
  540. $storeInfo['store_label'] = $storeProductLabelServices->getLabelCache($storeInfo['store_label_id'], ['id', 'label_name']);
  541. }
  542. if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) {
  543. /** @var StoreProductEnsureServices $storeProductEnsureServices */
  544. $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class);
  545. $storeInfo['ensure'] = $storeProductEnsureServices->getEnsurCache($storeInfo['ensure_id'], ['id', 'name', 'image', 'desc']);
  546. }
  547. /** @var QrcodeServices $qrcodeService */
  548. $qrcodeService = app()->make(QrcodeServices::class);
  549. $time = $request->param('time', '');
  550. $status = $request->param('status', '');
  551. $time_id = (int)$request->param('time_id', '');
  552. if (($configData['share_qrcode'] ?? 0) && request()->isWechat()) {
  553. $storeInfo['code_base'] = $qrcodeService->getTemporaryQrcode('seckill-' . $id . '-' . $time . '-' . $status, $uid)->url;
  554. } else {
  555. $storeInfo['code_base'] = $qrcodeService->getWechatQrcodePath($id . '_product_seckill_detail_wap.jpg', '/pages/activity/goods_seckill_details/index?id=' . $id . '&time=' . $time . '&status=' . $status);
  556. }
  557. /** @var StoreOrderServices $storeOrderServices */
  558. $storeOrderServices = app()->make(StoreOrderServices::class);
  559. $data['buy_num'] = $storeOrderServices->getBuyCount($uid, 1, $id);
  560. /** @var UserRelationServices $userRelationServices */
  561. $userRelationServices = app()->make(UserRelationServices::class);
  562. $storeInfo['userCollect'] = $userRelationServices->isProductRelation(['uid' => $uid, 'relation_id' => $storeInfo['product_id'], 'type' => 'collect', 'category' => UserRelationServices::CATEGORY_PRODUCT]);
  563. $storeInfo['userLike'] = 0;
  564. $storeInfo['uid'] = $uid;
  565. if ($storeInfo['quota'] > 0) {
  566. $percent = (float)sprintf("%.1f", ($storeInfo['quota_show'] - $storeInfo['quota']) / $storeInfo['quota_show'] * 100);
  567. $storeInfo['percent'] = $percent;
  568. $storeInfo['stock'] = $storeInfo['quota'];
  569. } else {
  570. $storeInfo['percent'] = 100;
  571. $storeInfo['stock'] = 0;
  572. }
  573. $storeInfo['last_time'] = 0;
  574. $time_id = $time_id ?: ($storeInfo['activity']['time_id'] ?? []);
  575. if ($time_id) {
  576. $time_id = is_string($time_id) ? explode(',', $time_id) : $time_id;
  577. /** @var StoreSeckillTimeServices $storeSeckillTimeServices */
  578. $storeSeckillTimeServices = app()->make(StoreSeckillTimeServices::class);
  579. $timeList = $storeSeckillTimeServices->getList(['id' => $time_id, 'status' => 1]);
  580. $config = [];
  581. $today = date('Y-m-d');
  582. $currentHour = date('Hi');
  583. if (count($timeList) <= 1) {
  584. $config = $timeList[0] ?? [];
  585. } else {
  586. foreach ($timeList as $value) {
  587. $start = str_replace(':', '', $value['start_time']);
  588. $end = str_replace(':', '', $value['end_time']);
  589. if ($currentHour >= $start && $currentHour < $end) {
  590. $config = $value;
  591. break;
  592. }
  593. }
  594. }
  595. //获取秒杀商品状态
  596. if ($storeInfo['status'] == 1) {
  597. if ($config) {//正在进行中的
  598. $start = str_replace(':', '', $config['start_time']);
  599. $end = str_replace(':', '', $config['end_time']);
  600. if ($start <= $currentHour && $end > $currentHour) {
  601. $storeInfo['status'] = 1;
  602. $storeInfo['last_time'] = strtotime($today. ' '. $config['end_time']);
  603. } else if ($start > $currentHour) {
  604. $storeInfo['status'] = 2;
  605. } else {
  606. $storeInfo['status'] = 0;
  607. }
  608. } else {
  609. $storeInfo['status'] = 0;
  610. }
  611. }
  612. }
  613. //商品详情
  614. $storeInfo['small_image'] = get_thumb_water($storeInfo['image']);
  615. $data['storeInfo'] = $storeInfo;
  616. $data['reply'] = [];
  617. $data['replyChance'] = $data['replyCount'] = 0;
  618. if (isset($infoDiy['is_reply']) && $infoDiy['is_reply']) {
  619. /** @var StoreProductReplyServices $storeProductReplyService */
  620. $storeProductReplyService = app()->make(StoreProductReplyServices::class);
  621. $reply = $storeProductReplyService->getRecProductReplyCache((int)$storeInfo['product_id'], (int)($infoDiy['reply_num'] ?? 1));
  622. $data['reply'] = $reply ? get_thumb_water($reply, 'small', ['pics']) : [];
  623. [$replyCount, $goodReply, $replyChance] = $storeProductReplyService->getProductReplyData((int)$storeInfo['product_id']);
  624. $data['replyChance'] = $replyChance;
  625. $data['replyCount'] = $replyCount;
  626. }
  627. /** @var StoreProductAttrServices $storeProductAttrServices */
  628. $storeProductAttrServices = app()->make(StoreProductAttrServices::class);
  629. [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetailCache($id, $uid, 0, 1, $storeInfo['product_id'], $productInfo);
  630. $data['productAttr'] = $productAttr;
  631. $data['productValue'] = $productValue;
  632. $data['routine_contact_type'] = $configData['routine_contact_type'] ?? 0;
  633. $data['store_func_status'] = (int)($configData['store_func_status'] ?? 1);//门店是否开启
  634. $data['store_self_mention'] = $data['store_func_status'] ? (int)($configData['store_self_mention'] ?? 0) : 0;//门店自提是否开启
  635. $data['site_name'] = $configData['site_name'] ?? '';
  636. $data['share_qrcode'] = $configData['share_qrcode'] ?? 0;
  637. $data['product_poster_title'] = $configData['product_poster_title'] ?? '';
  638. //浏览记录
  639. ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'seckill']);
  640. return $data;
  641. }
  642. /**
  643. * 修改秒杀库存
  644. * @param int $num
  645. * @param int $seckillId
  646. * @param string $unique
  647. * @param int $store_id
  648. * @return bool
  649. */
  650. public function decSeckillStock(int $num, int $seckillId, string $unique = '', int $store_id = 0)
  651. {
  652. $product_id = $this->dao->value(['id' => $seckillId], 'product_id');
  653. $res = true;
  654. if ($product_id) {
  655. if ($unique) {
  656. /** @var StoreProductAttrValueServices $skuValueServices */
  657. $skuValueServices = app()->make(StoreProductAttrValueServices::class);
  658. //减去秒杀商品的sku库存增加销量
  659. $res = $res && $skuValueServices->decProductAttrStock($seckillId, $unique, $num, 1);
  660. //减去当前普通商品sku的库存增加销量
  661. //秒杀商品sku
  662. $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk');
  663. //平台商品sku unique
  664. $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique');
  665. /** @var StoreProductServices $services */
  666. $services = app()->make(StoreProductServices::class);
  667. //减去普通商品库存
  668. $res = $res && $services->decProductStock($num, (int)$product_id, (string)$productUnique, $store_id);
  669. }
  670. //减去秒杀库存
  671. $res = $res && $this->dao->decStockIncSales(['id' => $seckillId, 'type' => 1], $num);
  672. }
  673. //更新单个缓存
  674. $info = $this->dao->getOne(['id' => $seckillId], '*', ['descriptions']);
  675. if ($info) {
  676. $info = $info->toArray();
  677. $this->dao->cacheUpdate($info);
  678. }
  679. return $res;
  680. }
  681. /**
  682. * 加库存减销量
  683. * @param int $num
  684. * @param int $seckillId
  685. * @param string $unique
  686. * @param int $store_id
  687. * @return bool
  688. */
  689. public function incSeckillStock(int $num, int $seckillId, string $unique = '', int $store_id = 0)
  690. {
  691. $product_id = $this->dao->value(['id' => $seckillId], 'product_id');
  692. $res = true;
  693. if ($product_id) {
  694. if ($unique) {
  695. /** @var StoreProductAttrValueServices $skuValueServices */
  696. $skuValueServices = app()->make(StoreProductAttrValueServices::class);
  697. //增加秒杀商品的sku库存减少销量
  698. $res = $res && $skuValueServices->incProductAttrStock($seckillId, $unique, $num, 1);
  699. //减去当前普通商品sku的库存增加销量
  700. //秒杀商品sku
  701. $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk');
  702. //平台商品sku unique
  703. $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique');
  704. /** @var StoreProductServices $services */
  705. $services = app()->make(StoreProductServices::class);
  706. //减去普通商品库存
  707. $res = $res && $services->incProductStock($num, (int)$product_id, (string)$productUnique, $store_id);
  708. }
  709. //增加秒杀库存减去销量
  710. $res = $res && $this->dao->incStockDecSales(['id' => $seckillId, 'type' => 1], $num);
  711. }
  712. //更新单个缓存
  713. $info = $this->dao->getOne(['id' => $seckillId], '*', ['descriptions']);
  714. if ($info) {
  715. $info = $info->toArray();
  716. $this->dao->cacheUpdate($info);
  717. }
  718. return $res;
  719. }
  720. /**
  721. * 下单|加入购物车验证秒杀商品库存
  722. * @param int $uid
  723. * @param int $seckillId
  724. * @param int $cartNum
  725. * @param string $unique
  726. * @return array
  727. * @throws \think\db\exception\DataNotFoundException
  728. * @throws \think\db\exception\DbException
  729. * @throws \think\db\exception\ModelNotFoundException
  730. */
  731. public function checkSeckillStock(int $uid, int $seckillId, int $cartNum = 1, int $store_id = 0, string $unique = '')
  732. {
  733. /** @var StoreProductAttrValueServices $attrValueServices */
  734. $attrValueServices = app()->make(StoreProductAttrValueServices::class);
  735. if ($unique == '') {
  736. $unique = $attrValueServices->value(['product_id' => $seckillId, 'type' => 1], 'unique');
  737. }
  738. //检查商品活动状态
  739. $storeSeckillinfo = $this->getSeckillCount($seckillId, '*,title as store_name');
  740. if (!$storeSeckillinfo) {
  741. throw new ValidateException('该活动已下架');
  742. }
  743. if ($storeSeckillinfo['once_num'] < $cartNum) {
  744. throw new ValidateException('每个订单限购' . $storeSeckillinfo['once_num'] . '件');
  745. }
  746. /** @var StoreOrderServices $orderServices */
  747. $orderServices = app()->make(StoreOrderServices::class);
  748. $userBuyCount = $orderServices->getBuyCount($uid, 1, $seckillId);
  749. if ($storeSeckillinfo['num'] < ($userBuyCount + $cartNum)) {
  750. throw new ValidateException('每人总共限购' . $storeSeckillinfo['num'] . '件');
  751. }
  752. if ($storeSeckillinfo['num'] < $cartNum) {
  753. throw new ValidateException('每人限购' . $storeSeckillinfo['num'] . '件');
  754. }
  755. $attrInfo = $attrValueServices->getOne(['product_id' => $seckillId, 'unique' => $unique, 'type' => 1]);
  756. if (!$attrInfo || $attrInfo['product_id'] != $seckillId) {
  757. throw new ValidateException('请选择有效的商品属性');
  758. }
  759. if ($cartNum > $attrInfo['quota']) {
  760. throw new ValidateException('该商品库存不足' . $cartNum);
  761. }
  762. if ($store_id) {
  763. /** @var StoreBranchProductServices $branchProductServices */
  764. $branchProductServices = app()->make(StoreBranchProductServices::class);
  765. $branchProductInfo = $branchProductServices->isValidStoreProduct((int)$storeSeckillinfo['product_id'], $store_id);
  766. if (!$branchProductInfo) {
  767. throw new ValidateException('门店该商品已下架');
  768. }
  769. $suk = $attrValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk');
  770. $brandAttrInfo = $attrValueServices->getOne(['product_id' => $branchProductInfo['id'], 'suk' => $suk, 'type' => 0]);
  771. if (!$brandAttrInfo) {
  772. throw new ValidateException('请选择有效的商品属性');
  773. }
  774. if ($cartNum > $brandAttrInfo['stock']) {
  775. throw new ValidateException('该商品库存不足' . $cartNum);
  776. }
  777. }
  778. return [$attrInfo, $unique, $storeSeckillinfo];
  779. }
  780. /**
  781. * 秒杀统计
  782. * @return array
  783. */
  784. public function seckillStatistics($id)
  785. {
  786. /** @var StoreOrderServices $orderServices */
  787. $orderServices = app()->make(StoreOrderServices::class);
  788. $pay_count = $orderServices->getDistinctCount(['type' => 1, 'activity_id' => $id, 'paid' => 1, 'pid' => [0, -1]], 'uid');
  789. $order_count = $orderServices->getDistinctCount(['type' => 1, 'activity_id' => $id, 'pid' => [0, -1]], 'uid');
  790. $all_price = $orderServices->sum(['type' => 1, 'activity_id' => $id, 'paid' => 1,'refund_type' => [0, 3], 'pid' => [0, -1]], 'pay_price');
  791. $seckillInfo = $this->dao->get($id);
  792. $pay_rate = $seckillInfo['quota'] . '/' . $seckillInfo['quota_show'];
  793. return compact('pay_count', 'order_count', 'all_price', 'pay_rate');
  794. }
  795. /**
  796. * 秒杀参与人统计
  797. * @param $id
  798. * @param string $keyword
  799. * @return array
  800. */
  801. public function seckillPeople($id, $keyword = '')
  802. {
  803. /** @var StoreOrderServices $orderServices */
  804. $orderServices = app()->make(StoreOrderServices::class);
  805. [$page, $limit] = $this->getPageValue();
  806. $list = $orderServices->seckillPeople($id, $keyword, $page, $limit);
  807. $count = $orderServices->getDistinctCount([['paid', '=', 1],['type', '=', 1],['activity_id', '=', $id], ['pid', 'in', [0, -1]], ['real_name|uid|user_phone', 'like', '%' . $keyword . '%']], 'uid', false);
  808. foreach ($list as &$item) {
  809. $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']);
  810. }
  811. return compact('list', 'count');
  812. }
  813. /**
  814. * 秒杀订单统计
  815. * @param $id
  816. * @param array $where
  817. * @return array
  818. */
  819. public function seckillOrder(int $id, array $where = [])
  820. {
  821. /** @var StoreOrderServices $orderServices */
  822. $orderServices = app()->make(StoreOrderServices::class);
  823. [$page, $limit] = $this->getPageValue();
  824. $list = $orderServices->activityStatisticsOrder($id, 1, $where, $page, $limit);
  825. $where['type'] = 1;
  826. $where['activity_id'] = $id;
  827. $count = $orderServices->count($where);
  828. foreach ($list as &$item) {
  829. if ($item['is_del'] || $item['is_system_del']) {
  830. $item['status'] = '已删除';
  831. } else if ($item['paid'] == 0 && $item['status'] == 0) {
  832. $item['status'] = '未支付';
  833. } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) {
  834. $item['status'] = '部分发货';
  835. } else if ($item['paid'] == 1 && $item['refund_status'] == 2) {
  836. $item['status'] = '已退款';
  837. } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['refund_status'] == 0) {
  838. $item['status'] = $item['shipping_type'] == 2 ? '部分核销' : '部分收货';
  839. $item['_status'] = 12;//已支付 部分核销
  840. } else if ($item['paid'] == 1 && $item['refund_status'] == 1) {
  841. $item['status'] = '申请退款';
  842. } else if ($item['paid'] == 1 && $item['refund_status'] == 4) {
  843. $item['status'] = '退款中';
  844. } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) {
  845. $item['status'] = '未发货';
  846. $item['_status'] = 2;//已支付 未发货
  847. } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) {
  848. $item['status'] = '未核销';
  849. } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) {
  850. $item['status'] = '待收货';
  851. } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) {
  852. $item['status'] = '待评价';
  853. } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) {
  854. $item['status'] = '已完成';
  855. } else if ($item['paid'] == 1 && $item['refund_status'] == 3) {
  856. $item['status'] = '部分退款';
  857. } else {
  858. $item['status'] = '未知';
  859. }
  860. $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']);
  861. $item['pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : '';
  862. }
  863. return compact('list', 'count');
  864. }
  865. }