Activity.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <?php
  2. namespace app\admin\controller\shopro\activity;
  3. use app\common\controller\Backend;
  4. use think\Db;
  5. use think\exception\PDOException;
  6. use think\exception\ValidateException;
  7. use Exception;
  8. use app\admin\controller\shopro\Base;
  9. use addons\shopro\library\traits\ActivityCache;
  10. /**
  11. * 营销活动
  12. *
  13. * @icon fa fa-circle-o
  14. */
  15. class Activity extends Base
  16. {
  17. use ActivityCache;
  18. /**
  19. * Activity模型对象
  20. * @var \app\admin\model\shopro\activity\Activity
  21. */
  22. protected $model = null;
  23. public function _initialize()
  24. {
  25. parent::_initialize();
  26. $this->model = new \app\admin\model\shopro\activity\Activity;
  27. $this->assignconfig("hasRedis", $this->hasRedis()); // 检测是否配置 redis
  28. }
  29. /**
  30. * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
  31. * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
  32. * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
  33. */
  34. /**
  35. * 查看活动列表
  36. */
  37. public function index()
  38. {
  39. //设置过滤方法
  40. $this->request->filter(['strip_tags']);
  41. if ($this->request->isAjax()) {
  42. // 检测队列
  43. checkEnv('queue');
  44. //如果发送的来源是Selectpage,则转发到Selectpage
  45. if ($this->request->request('keyField')) {
  46. return $this->selectpage();
  47. }
  48. $nobuildfields = ['activitytime', 'status'];
  49. list($where, $sort, $order, $offset, $limit) = $this->custombuildparams(['title'], $nobuildfields);
  50. $total = $this->buildSearch()
  51. ->where($where)
  52. ->order($sort, $order)
  53. ->count();
  54. $list = $this->buildSearch()
  55. ->where($where)
  56. ->order($sort, $order)
  57. ->limit($offset, $limit)
  58. ->select();
  59. $list = collection($list)->toArray();
  60. // 关联活动的商品
  61. $goodsIds = array_column($list, 'goods_ids');
  62. $goodsIdsArr = [];
  63. foreach($goodsIds as $ids) {
  64. $idsArr = explode(',', $ids);
  65. $goodsIdsArr = array_merge($goodsIdsArr, $idsArr);
  66. }
  67. $goodsIdsArr = array_values(array_filter(array_unique($goodsIdsArr)));
  68. if ($goodsIdsArr) {
  69. // 查询商品
  70. $goods = \app\admin\model\shopro\goods\Goods::where('id', 'in', $goodsIdsArr)->select();
  71. $goods = array_column($goods, null, 'id');
  72. }
  73. foreach ($list as $key => $activity) {
  74. $list[$key]['goods'] = [];
  75. $idsArr = explode(',', $activity['goods_ids']);
  76. foreach ($idsArr as $id) {
  77. if (isset($goods[$id])) {
  78. $list[$key]['goods'][] = $goods[$id];
  79. }
  80. }
  81. }
  82. $result = array("total" => $total, "rows" => $list);
  83. if ($this->request->get("page_type") == 'select') {
  84. return json($result);
  85. }
  86. return $this->success('操作成功', null, $result);
  87. }
  88. return $this->view->fetch();
  89. }
  90. public function all() {
  91. if ($this->request->isAjax()) {
  92. $type = $this->request->get('type', 'all');
  93. $sort = $this->request->get("sort", !empty($this->model) && $this->model->getPk() ? $this->model->getPk() : 'id');
  94. $order = $this->request->get("order", "DESC");
  95. $activities = $this->model->withTrashed(); // 包含被删除的
  96. if ($type != 'all') {
  97. $activities = $activities->where('type', $type);
  98. }
  99. $activities = $activities
  100. ->field('id, title, type, starttime, endtime, rules')
  101. ->order($sort, $order)
  102. ->select();
  103. $activities = collection($activities)->toArray();
  104. return $this->success('操作成功', null, $activities);
  105. }
  106. }
  107. // 获取活动的选项
  108. public function getType()
  109. {
  110. $activity_type = (new \app\admin\model\shopro\activity\Activity)->getTypeList();
  111. $activity_status = (new \app\admin\model\shopro\activity\Activity)->getStatusList();
  112. $result = [
  113. 'activity_type' => $activity_type,
  114. 'activity_status' => $activity_status,
  115. ];
  116. $data = [];
  117. foreach ($result as $key => $list) {
  118. $data[$key][] = ['name' => '全部', 'type' => 'all'];
  119. foreach ($list as $k => $v) {
  120. $data[$key][] = [
  121. 'name' => $v,
  122. 'type' => $k
  123. ];
  124. }
  125. }
  126. return $this->success('操作成功', null, $data);
  127. }
  128. /**
  129. * 添加
  130. */
  131. public function add()
  132. {
  133. if ($this->request->isPost()) {
  134. $params = $this->request->post();
  135. if ($params) {
  136. $params = $this->preExcludeFields($params);
  137. if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
  138. $params[$this->dataLimitField] = $this->auth->id;
  139. }
  140. $result = false;
  141. Db::startTrans();
  142. try {
  143. //是否采用模型验证
  144. if ($this->modelValidate) {
  145. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  146. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
  147. $this->model->validateFailException(true)->validate($validate);
  148. }
  149. // 检测活动是否可以正常添加
  150. $this->checkActivity($params);
  151. $params['rules'] = json_encode($params['rules']);
  152. $result = $this->model->allowField(true)->save($params);
  153. if (in_array($params['type'], ['groupon', 'seckill'])) {
  154. // 秒杀拼团,更新规格
  155. $this->createOrUpdateSku($params['goods_list'], $this->model->id);
  156. }
  157. // 活动创建修改后
  158. $data = [
  159. 'activity' => $this->model
  160. ];
  161. \think\Hook::listen('activity_update_after', $data);
  162. Db::commit();
  163. } catch (ValidateException $e) {
  164. Db::rollback();
  165. $this->error($e->getMessage());
  166. } catch (PDOException $e) {
  167. Db::rollback();
  168. $this->error($e->getMessage());
  169. } catch (Exception $e) {
  170. Db::rollback();
  171. $this->error($e->getMessage());
  172. }
  173. if ($result !== false) {
  174. $this->success();
  175. } else {
  176. $this->error(__('No rows were inserted'));
  177. }
  178. }
  179. $this->error(__('Parameter %s can not be empty', ''));
  180. }
  181. return $this->view->fetch('edit');
  182. }
  183. /**
  184. * 添加,编辑活动规格,type = stock 只编辑库存
  185. *
  186. * @param array $goodsList 商品列表
  187. * @param int $activity_id 活动 id
  188. * @param string $type type = all 全部编辑,type = stock 只编辑库存
  189. * @return void
  190. */
  191. protected function createOrUpdateSku($goodsList, $activity_id, $type = 'all')
  192. {
  193. //如果是编辑 先下架所有的规格产品,防止丢失历史销量数据;
  194. \app\admin\model\shopro\activity\ActivitySkuPrice::where(['activity_id' => $activity_id])->update(['status' => 'down']);
  195. $list = [];
  196. foreach ($goodsList as $k => $g) {
  197. $actSkuPrice[$k] = json_decode($g['actSkuPrice'], true);
  198. foreach ($actSkuPrice[$k] as $a => $c) {
  199. if ($type == 'all') {
  200. $current = $c;
  201. } else {
  202. $current = [
  203. 'id' => $c['id'],
  204. 'stock' => $c['stock'],
  205. 'status' => $c['status']
  206. ];
  207. }
  208. if ($current['id'] == 0) {
  209. unset($current['id']);
  210. }
  211. unset($current['sales']);
  212. $current['activity_id'] = $activity_id;
  213. $current['goods_id'] = $g['id'];
  214. $list[] = $current;
  215. }
  216. }
  217. $act = new \app\admin\model\shopro\activity\ActivitySkuPrice;
  218. $act->allowField(true)->saveAll($list);
  219. }
  220. /**
  221. * 编辑
  222. */
  223. public function edit($ids = null)
  224. {
  225. //编辑
  226. $row = $this->model->get($ids);
  227. if (!$row) {
  228. $this->error(__('No Results were found'));
  229. }
  230. $adminIds = $this->getDataLimitAdminIds();
  231. if (is_array($adminIds)) {
  232. if (!in_array($row[$this->dataLimitField], $adminIds)) {
  233. $this->error(__('You have no permission'));
  234. }
  235. }
  236. if ($this->request->isPost()) {
  237. $params = $this->request->post();
  238. if ($params) {
  239. $params = $this->preExcludeFields($params);
  240. $result = false;
  241. Db::startTrans();
  242. try {
  243. //是否采用模型验证
  244. if ($this->modelValidate) {
  245. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  246. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
  247. $row->validateFailException(true)->validate($validate);
  248. }
  249. // 检测活动是否可以正常添加
  250. $this->checkActivity($params, $row->id);
  251. $params['rules'] = json_encode($params['rules']);
  252. if ($row['status'] == 'ing') {
  253. // 活动正在进行中,只能编辑活动结束时间
  254. $params = [
  255. 'type' => $params['type'],
  256. 'endtime' => $params['endtime'],
  257. 'goods_list' => $params['goods_list'],
  258. ];
  259. }
  260. $result = $row->allowField(true)->save($params);
  261. if (in_array($params['type'], ['groupon', 'seckill'])) {
  262. // 秒杀拼团,更新规格
  263. $this->createOrUpdateSku($params['goods_list'], $row->id, ($row['status'] == 'ing' ? 'stock' : 'all'));
  264. }
  265. // 活动创建修改后
  266. $data = [
  267. 'activity' => $row
  268. ];
  269. \think\Hook::listen('activity_update_after', $data);
  270. Db::commit();
  271. } catch (ValidateException $e) {
  272. Db::rollback();
  273. $this->error($e->getMessage());
  274. } catch (PDOException $e) {
  275. Db::rollback();
  276. $this->error($e->getMessage());
  277. } catch (Exception $e) {
  278. Db::rollback();
  279. $this->error($e->getMessage());
  280. }
  281. if ($result !== false) {
  282. $this->success();
  283. } else {
  284. $this->error(__('No rows were updated'));
  285. }
  286. }
  287. $this->error(__('Parameter %s can not be empty', ''));
  288. }
  289. $goods_ids_array = array_filter(explode(',', $row->goods_ids));
  290. $goodsList = [];
  291. foreach ($goods_ids_array as $k => $g) {
  292. $goods[$k] = \app\admin\model\shopro\goods\Goods::field('id,title,image')->where('id', $g)->find();
  293. $goods[$k]['actSkuPrice'] = json_encode(\app\admin\model\shopro\activity\ActivitySkuPrice::all(['goods_id' => $g, 'activity_id' => $ids]));
  294. $goods[$k]['opt'] = 1;
  295. $goodsList[] = $goods[$k];
  296. }
  297. $row->goods_list = $goodsList;
  298. $this->assignconfig("activity", $row);
  299. $this->view->assign("row", $row);
  300. $this->assignconfig('id', $ids);
  301. return $this->view->fetch();
  302. }
  303. /**
  304. * 选择活动
  305. */
  306. public function select()
  307. {
  308. if ($this->request->isAjax()) {
  309. return $this->index();
  310. }
  311. return $this->view->fetch();
  312. }
  313. /**
  314. * 获取活动规则
  315. *
  316. * @param int $id 商品 id
  317. * @param int $activity_id 活动 id
  318. * @param string $type 类型
  319. * @return void
  320. */
  321. public function sku()
  322. {
  323. $id = $this->request->get('id', 0);
  324. $activity_id = $this->request->get('activity_id', 0);
  325. $activity_type = $this->request->get('activity_type', '');
  326. $type = $this->request->get('type', 0);
  327. $activitytime = $this->request->get('activitytime', '') ? $this->request->get('activitytime', '') : '';
  328. $activitytime = array_filter(explode(' - ', $activitytime));
  329. if (in_array($type, ['add', 'edit']) && $activitytime && $activity_type) {
  330. // 如果存在开始结束时间,并且是要修改
  331. $goodsList = [$id => []];
  332. try {
  333. $this->checkGoods($goodsList, [
  334. 'type' => $activity_type,
  335. 'starttime' => $activitytime[0],
  336. 'endtime' => $activitytime[1]
  337. ], $activity_id);
  338. } catch(\Exception $e) {
  339. $this->error(preg_replace('/^部分商品/', '该商品', $e->getMessage()), '');
  340. }
  341. }
  342. // 商品规格
  343. $skuList = \app\admin\model\shopro\goods\Sku::with(['children' => function ($query) use ($id) {
  344. $query->where('goods_id', $id);
  345. }])->where(['pid' => 0, 'goods_id' => $id])->select();
  346. // 获取规格
  347. $skuPrice = \app\admin\model\shopro\goods\SkuPrice::with(['activitySkuPrice' => function ($query) use ($activity_id) {
  348. $query->where('activity_id', $activity_id);
  349. }])->where(['goods_id' => $id])->select();
  350. //编辑
  351. $actSkuPrice = [];
  352. foreach ($skuPrice as $k => &$p) {
  353. $actSkuPrice[$k] = $p['activity_sku_price'];
  354. if (!$actSkuPrice[$k]) {
  355. $actSkuPrice[$k]['id'] = 0;
  356. $actSkuPrice[$k]['status'] = 'down';
  357. $actSkuPrice[$k]['price'] = '';
  358. $actSkuPrice[$k]['stock'] = '';
  359. $actSkuPrice[$k]['sales'] = '0';
  360. $actSkuPrice[$k]['sku_price_id'] = $p['id'];
  361. }
  362. }
  363. $this->assignconfig('skuList', $skuList);
  364. $this->assignconfig('skuPrice', $skuPrice);
  365. $this->assignconfig('actSkuPrice', $actSkuPrice);
  366. return $this->view->fetch();
  367. }
  368. /**
  369. * 删除
  370. */
  371. public function del($ids = "")
  372. {
  373. if ($ids) {
  374. $pk = $this->model->getPk();
  375. $adminIds = $this->getDataLimitAdminIds();
  376. if (is_array($adminIds)) {
  377. $this->model->where($this->dataLimitField, 'in', $adminIds);
  378. }
  379. $list = $this->model->where($pk, 'in', $ids)->select();
  380. $count = 0;
  381. Db::startTrans();
  382. try {
  383. foreach ($list as $k => $v) {
  384. $count += $v->delete();
  385. // 删除之后事件
  386. $data = [
  387. 'activity' => $v
  388. ];
  389. \think\Hook::listen('activity_delete_after', $data);
  390. }
  391. Db::commit();
  392. } catch (PDOException $e) {
  393. Db::rollback();
  394. $this->error($e->getMessage());
  395. } catch (Exception $e) {
  396. Db::rollback();
  397. $this->error($e->getMessage());
  398. }
  399. if ($count) {
  400. $this->success();
  401. } else {
  402. $this->error(__('No rows were deleted'));
  403. }
  404. }
  405. $this->error(__('Parameter %s can not be empty', 'ids'));
  406. }
  407. // 构建查询条件
  408. private function buildSearch()
  409. {
  410. $filter = $this->request->get("filter", '');
  411. $filter = (array)json_decode($filter, true);
  412. $filter = $filter ? $filter : [];
  413. $status = isset($filter['status']) ? $filter['status'] : 'all';
  414. $activitytime = isset($filter['activitytime']) ? $filter['activitytime'] : '';
  415. $activitytime = array_filter(explode(' - ', $activitytime));
  416. $name = $this->model->getQuery()->getTable();
  417. $tableName = $name . '.';
  418. $activities = $this->model;
  419. // 活动状态
  420. if ($status != 'all') {
  421. $where = [];
  422. if ($status == 'ing') {
  423. $where['starttime'] = ['<', time()];
  424. $where['endtime'] = ['>', time()];
  425. } else if ($status == 'nostart') {
  426. $where['starttime'] = ['>', time()];
  427. } else if ($status == 'ended') {
  428. $where['endtime'] = ['<', time()];
  429. }
  430. $activities = $activities->where($where);
  431. }
  432. if ($activitytime) {
  433. $activities = $activities->where('starttime', '>=', strtotime($activitytime[0]))->where('endtime', '<=', strtotime($activitytime[1]));
  434. }
  435. return $activities;
  436. }
  437. private function checkActivity($params, $activity_id = 0)
  438. {
  439. if (empty($params['type'])) {
  440. throw Exception('请选择活动类型');
  441. }
  442. if ($params['starttime'] > $params['endtime'] || $params['endtime'] < date('Y-m-d H:i:s')) {
  443. throw Exception('请设置正确的活动时间');
  444. }
  445. if (in_array($params['type'], ['full_reduce', 'full_discount'])) {
  446. if (!$params['rules'] || !isset($params['rules']['discounts']) || !$params['rules']['discounts']) {
  447. throw Exception('请设置优惠条件');
  448. }
  449. }
  450. $goodsList = [];
  451. if ($params['goods_ids']) { // 部分商品
  452. // 检测要设置商品是否存在活动重合
  453. foreach ($params['goods_list'] as $key => $goods) {
  454. if (in_array($params['type'], ['groupon', 'seckill'])) {
  455. $actSkuPrice = json_decode($goods['actSkuPrice'], true);
  456. if (!$actSkuPrice) {
  457. throw Exception('请至少将商品一个规格设置为活动规格');
  458. }
  459. }
  460. $goodsList[$goods['id']] = $goods;
  461. }
  462. }
  463. // 检测商品是否在别的活动被设置
  464. $this->checkGoods($goodsList, $params, $activity_id);
  465. }
  466. /**
  467. * 检测活动商品是否重合
  468. *
  469. * @return void
  470. */
  471. private function checkGoods($goodsList = [], $params, $activity_id = 0)
  472. {
  473. $starttime = strtotime($params['starttime']);
  474. $endtime = strtotime($params['endtime']);
  475. $goodsIds = array_keys($goodsList);
  476. // 如果拼团秒杀,当前活动结束时间要包含活动下架时间
  477. if (in_array($params['type'], ['groupon', 'seckill'])) {
  478. $current_activity_auto_close = isset($params['rules']['activity_auto_close']) ? intval($params['rules']['activity_auto_close']) : 0;
  479. $current_activity_auto_close = $current_activity_auto_close > 0 ? ($current_activity_auto_close * 60) : 0;
  480. $endtime += $current_activity_auto_close;
  481. }
  482. // 获取所有活动
  483. $activities = $this->getActivities($params['type']);
  484. foreach ($activities as $key => $activity) {
  485. if ($activity_id && $activity_id == $activity['id']) {
  486. // 编辑的时候,把自己排除在外
  487. continue;
  488. }
  489. $intersect = []; // 两个活动重合的商品Ids
  490. if ($goodsIds) {
  491. $activityGoodsIds = array_filter(explode(',', $activity['goods_ids']));
  492. // 不是全部商品,并且不重合
  493. if ($activityGoodsIds && !$intersect = array_intersect($activityGoodsIds, $goodsIds)) {
  494. // 商品不重合,继续验证下个活动
  495. continue;
  496. }
  497. }
  498. // 如果活动设置的有活动结束继续显示时间,则检验活动冲突结束时间要加上活动下架时间
  499. $activity_starttime = $activity['starttime'];
  500. $activity_endtime = $activity['endtime'];
  501. if (in_array($activity['type'], ['seckill', 'groupon'])) {
  502. // 结束时间加上活动自动下架时间
  503. $activity_auto_close = isset($activity['rules']['activity_auto_close']) ? intval($activity['rules']['activity_auto_close']) : 0;
  504. $activity_auto_close = $activity_auto_close > 0 ? ($activity_auto_close * 60) : 0;
  505. $activity_endtime += $activity_auto_close;
  506. }
  507. if ($endtime <= $activity_starttime || $starttime >= $activity_endtime) {
  508. // 设置的时间在当前活动开始之前,或者在当前结束时间之后
  509. continue;
  510. }
  511. $goods_names = '';
  512. foreach ($intersect as $id) {
  513. if (isset($goodsList[$id]) && isset($goodsList[$id]['title'])) {
  514. $goods_names .= $goodsList[$id]['title'] . ',';
  515. }
  516. }
  517. if ($goods_names) {
  518. $goods_names = mb_strlen($goods_names) > 40 ? mb_substr($goods_names, 0, 37) . '...' : $goods_names;
  519. }
  520. throw Exception('部分商品' . ($goods_names ? ' ' . $goods_names . ' ' : '') . ' 已在 ' . $activity['title'] . ' 活动中设置');
  521. }
  522. }
  523. /**
  524. * 获取所有活动
  525. *
  526. * @return array
  527. */
  528. private function getActivities($current_activity_type) {
  529. // 获取当前活动的互斥活动
  530. $activityTypes = $this->getMutexActivityType($current_activity_type);
  531. // 获取所有活动
  532. if ($this->hasRedis()) {
  533. // 如果有redis 读取 redis
  534. $activities = $this->getActivityList($activityTypes, 'all', 'clear');
  535. return $activities;
  536. }
  537. // 没有配置 redis,查询所有活动
  538. $activities = $this->model->where('type', 'in', $activityTypes)->select();
  539. return $activities;
  540. }
  541. /**
  542. * 获取当前要添加的活动的互斥活动列表
  543. *
  544. * @param [type] $current_activity_type
  545. * @return void
  546. */
  547. private function getMutexActivityType($current_activity_type) {
  548. $activityTypes = [];
  549. switch($current_activity_type) {
  550. case 'seckill':
  551. // full_reduce full_discount 先不考虑,在获取活动时候,就不会获取这两个了
  552. $activityTypes = ['seckill', 'groupon'];
  553. break;
  554. case 'groupon':
  555. // full_reduce full_discount 先不考虑,在获取活动时候,就不会获取这两个了
  556. $activityTypes = ['seckill', 'groupon'];
  557. break;
  558. case 'full_reduce':
  559. // seckill groupon 先不考虑,在获取活动时候,如果是拼团秒杀,则full_reduce就不会获取了
  560. $activityTypes = ['full_reduce', 'full_discount'];
  561. break;
  562. case 'full_discount':
  563. // seckill groupon 先不考虑,在获取活动时候,如果是拼团秒杀,则full_discount就不会获取了
  564. $activityTypes = ['full_reduce', 'full_discount'];
  565. break;
  566. case 'free_shipping':
  567. $activityTypes = ['free_shipping'];
  568. break;
  569. }
  570. return $activityTypes;
  571. }
  572. }