Discount.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. <?php
  2. namespace app\admin\controller\marketing;
  3. use app\common\controller\Backend;
  4. use think\Db;
  5. use Exception;
  6. use think\exception\DbException;
  7. use think\exception\PDOException;
  8. use think\exception\ValidateException;
  9. use app\common\Enum\ChannelEnum;
  10. use app\common\Enum\ActivityEnum;
  11. /**
  12. * 营销活动表(整体活动)
  13. *
  14. * @icon fa fa-circle-o
  15. */
  16. class Discount extends Backend
  17. {
  18. /**
  19. * Discount模型对象
  20. * @var \app\admin\model\marketing\discount\Discount
  21. */
  22. protected $model = null;
  23. public function _initialize()
  24. {
  25. parent::_initialize();
  26. $this->model = new \app\admin\model\marketing\discount\Discount;
  27. // 渠道配置
  28. $this->view->assign("channelList", ChannelEnum::getChannelMap());
  29. $this->assignconfig("channelList", json_encode(ChannelEnum::getChannelMap()));
  30. }
  31. /**
  32. * 规格折扣设置
  33. */
  34. public function spec_discount()
  35. {
  36. $goods_id = $this->request->get('goods_id');
  37. if (!$goods_id) {
  38. $this->error('参数错误');
  39. }
  40. if ($this->request->isAjax()) {
  41. // 处理提交的数据
  42. $specs = $this->request->post('specs', []);
  43. if ($specs) {
  44. $result = [
  45. 'goodsId' => $goods_id,
  46. 'specs' => $specs
  47. ];
  48. $this->success('设置成功', null, $result);
  49. }
  50. $this->error('提交数据失败');
  51. }
  52. // 获取商品信息
  53. $goods = Db::name('shop_goods')->where('id', $goods_id)->find();
  54. if (!$goods) {
  55. $this->error('商品不存在');
  56. }
  57. // 获取规格信息
  58. $goods_skus = Db::name('shop_goods_sku')
  59. ->where('goods_id', $goods_id)
  60. ->select();
  61. // 获取规格属性名称和值
  62. $sku_specs = [];
  63. foreach ($goods_skus as &$sku) {
  64. $sku_id_arr = explode(',', $sku['spec_value_ids']);
  65. // 查询规格名称和值
  66. $spec_values = Db::name('shop_goods_sku_spec')
  67. ->alias('p')
  68. ->field('p.*, sp.name as spec_name, sv.value as spec_value')
  69. ->join('shop_spec sp', 'sp.id=p.spec_id', 'LEFT')
  70. ->join('shop_spec_value sv', 'sv.id=p.spec_value_id', 'LEFT')
  71. ->where('p.id', 'in', $sku_id_arr)
  72. ->select();
  73. $specs_text = [];
  74. foreach ($spec_values as $spec) {
  75. $specs_text[] = $spec['spec_name'] . ':' . $spec['spec_value'];
  76. }
  77. $sku['specs_text'] = implode(' | ', $specs_text);
  78. }
  79. $this->view->assign('goods', $goods);
  80. $this->view->assign('skus', $goods_skus);
  81. return $this->view->fetch();
  82. }
  83. /**
  84. * 添加
  85. *
  86. * @return string
  87. * @throws \think\Exception
  88. */
  89. public function add()
  90. {
  91. if (false === $this->request->isPost()) {
  92. return $this->view->fetch();
  93. }
  94. $params = $this->request->post('row/a');
  95. if (empty($params)) {
  96. $this->error(__('Parameter %s can not be empty', ''));
  97. }
  98. $params = $this->preExcludeFields($params);
  99. if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
  100. $params[$this->dataLimitField] = $this->auth->id;
  101. }
  102. // 处理活动渠道
  103. if (isset($params['channels']) && is_array($params['channels'])) {
  104. $params['channels'] = json_encode($params['channels']);
  105. } else {
  106. $params['channels'] = json_encode([]);
  107. }
  108. // 处理商品ID和商品数量
  109. $goodsIds = isset($params['goods_ids']) ? $params['goods_ids'] : '';
  110. if ($goodsIds) {
  111. $goodsIdArr = explode(',', $goodsIds);
  112. $params['goods_count'] = count($goodsIdArr);
  113. // 存储为JSON格式
  114. $params['goods_ids'] = json_encode($goodsIdArr);
  115. } else {
  116. $params['goods_count'] = 0;
  117. $params['goods_ids'] = '[]';
  118. }
  119. // 设置默认字段值
  120. $params['type'] = isset($params['type']) ? $params['type'] : 'discount';
  121. $params['inner_type'] = isset($params['inner_type']) ? $params['inner_type'] : 0;
  122. // 对于折扣活动,强制设置为指定商品参与
  123. if ($params['type'] == 'discount') {
  124. $params['goods_join_type'] = 2; // 指定商品参与
  125. } else {
  126. $params['goods_join_type'] = isset($params['goods_join_type']) ? $params['goods_join_type'] : 0;
  127. }
  128. // 设置活动状态
  129. $now = time();
  130. $startTime = strtotime($params['start_time']);
  131. $endTime = strtotime($params['end_time']);
  132. if ($startTime > $now) {
  133. $params['activity_status'] = 0; // 未开始
  134. } elseif ($startTime <= $now && $endTime > $now) {
  135. $params['activity_status'] = 1; // 进行中
  136. } else {
  137. $params['activity_status'] = 2; // 已结束
  138. }
  139. // 检查是否与其他活动时间冲突(同一时间只能有一个进行中的活动)
  140. $this->checkActivityTimeConflict($startTime, $endTime);
  141. $result = false;
  142. Db::startTrans();
  143. try {
  144. // 是否采用模型验证
  145. if ($this->modelValidate) {
  146. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  147. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
  148. $this->model->validateFailException()->validate($validate);
  149. }
  150. // 保存活动基本信息
  151. $result = $this->model->allowField(true)->save($params);
  152. // 处理折扣信息
  153. $discountId = $this->model->id;
  154. // 保存规格折扣数据
  155. if ($params['type'] == 'discount') {
  156. $result = $this->processActivityGoodsData($discountId, $params);
  157. if (!$result['success']) {
  158. throw new Exception($result['message']);
  159. }
  160. }
  161. Db::commit();
  162. } catch (ValidateException|PDOException|Exception $e) {
  163. Db::rollback();
  164. $this->error($e->getMessage());
  165. }
  166. if ($result === false) {
  167. $this->error(__('No rows were inserted'));
  168. }
  169. $this->success();
  170. }
  171. /**
  172. * 编辑
  173. *
  174. * @param $ids
  175. * @return string
  176. * @throws DbException
  177. * @throws \think\Exception
  178. */
  179. public function edit($ids = null)
  180. {
  181. $row = $this->model->get($ids);
  182. if (!$row) {
  183. $this->error(__('No Results were found'));
  184. }
  185. $adminIds = $this->getDataLimitAdminIds();
  186. if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
  187. $this->error(__('You have no permission'));
  188. }
  189. if (false === $this->request->isPost()) {
  190. // 解码JSON数据
  191. if ($row['channels']) {
  192. $row['channels'] = json_decode($row['channels'], true);
  193. }
  194. if ($row['goods_ids']) {
  195. $goodsIdArr = json_decode($row['goods_ids'], true);
  196. $row['goods_ids'] = implode(',', $goodsIdArr);
  197. // 获取商品数据,包含完整的SKU和规格信息
  198. $goodsData = [];
  199. if (!empty($goodsIdArr)) {
  200. // 查询商品基础数据
  201. $goodsResult = Db::name('shop_goods')
  202. ->where('id', 'in', $goodsIdArr)
  203. ->select();
  204. $goodsData = [];
  205. if ($goodsResult) {
  206. foreach ($goodsResult as $item) {
  207. $goodsData[] = is_array($item) ? $item : $item->toArray();
  208. }
  209. }
  210. // 一次性查询该活动的所有SKU折扣数据
  211. $activitySkus = Db::table('shop_activity_sku')
  212. ->where('activity_id', $ids)
  213. ->select();
  214. // 将活动SKU数据按照goods_id和sku_id进行索引
  215. $activitySkuMap = [];
  216. foreach ($activitySkus as $activitySku) {
  217. $key = $activitySku['goods_id'] . '_' . $activitySku['sku_id'];
  218. $activitySkuMap[$key] = $activitySku;
  219. }
  220. // 一次性查询所有商品的SKU信息
  221. $allSkusResult = Db::name('shop_goods_sku')
  222. ->where('goods_id', 'in', $goodsIdArr)
  223. ->order('is_default desc, id asc')
  224. ->select();
  225. // 转换SKU结果为数组并按商品ID分组
  226. $skusByGoods = [];
  227. foreach ($allSkusResult as $sku) {
  228. $skuArray = is_array($sku) ? $sku : $sku->toArray();
  229. $skusByGoods[$skuArray['goods_id']][] = $skuArray;
  230. }
  231. // 找出多规格商品ID
  232. $multiSpecGoodsIds = [];
  233. foreach ($goodsData as $item) {
  234. if ($item['spec_type'] == 1) {
  235. $multiSpecGoodsIds[] = $item['id'];
  236. }
  237. }
  238. // 一次性查询所有多规格商品的规格信息
  239. $specsByGoods = [];
  240. if (!empty($multiSpecGoodsIds)) {
  241. $allSpecsResult = Db::name('shop_goods_sku_spec')
  242. ->alias('gss')
  243. ->field('gss.goods_id, sp.id as spec_id, sp.name as spec_name, sp.type as spec_type, sv.id as spec_value_id, sv.value, sv.image, sv.desc')
  244. ->join('shop_spec sp', 'sp.id = gss.spec_id', 'LEFT')
  245. ->join('shop_spec_value sv', 'sv.id = gss.spec_value_id', 'LEFT')
  246. ->where('gss.goods_id', 'in', $multiSpecGoodsIds)
  247. ->order('gss.goods_id asc, sp.id asc, sv.id asc')
  248. ->select();
  249. // 按商品ID分组规格数据
  250. foreach ($allSpecsResult as $spec) {
  251. $specArray = is_array($spec) ? $spec : $spec->toArray();
  252. $specsByGoods[$specArray['goods_id']][] = $specArray;
  253. }
  254. }
  255. // 一次性查询所有SKU的规格属性文本
  256. $allSkuIds = [];
  257. foreach ($skusByGoods as $skus) {
  258. foreach ($skus as $sku) {
  259. $allSkuIds[] = $sku['id'];
  260. }
  261. }
  262. $skuSpecTexts = [];
  263. if (!empty($allSkuIds)) {
  264. $skuSpecResults = Db::name('shop_goods_sku')
  265. ->alias('sku')
  266. ->field('sku.id, GROUP_CONCAT(sp.name,":",sv.value ORDER BY sp.id asc) as specs_text')
  267. ->join('shop_goods_sku_spec gss', "FIND_IN_SET(gss.id, sku.spec_value_ids)", 'LEFT')
  268. ->join('shop_spec sp', 'sp.id = gss.spec_id', 'LEFT')
  269. ->join('shop_spec_value sv', 'sv.id = gss.spec_value_id', 'LEFT')
  270. ->where('sku.id', 'in', $allSkuIds)
  271. ->group('sku.id')
  272. ->select();
  273. foreach ($skuSpecResults as $result) {
  274. $resultArray = is_array($result) ? $result : $result->toArray();
  275. $skuSpecTexts[$resultArray['id']] = $resultArray['specs_text'] ?: '';
  276. }
  277. }
  278. // 为每个商品添加完整的SKU和规格信息(纯数据组装,无数据库查询)
  279. foreach ($goodsData as &$item) {
  280. if ($item['image']) {
  281. $item['image'] = cdnurl($item['image'], true);
  282. }
  283. // 获取商品的SKU信息
  284. $skus = $skusByGoods[$item['id']] ?? [];
  285. if ($skus) {
  286. // 获取第一个SKU ID(单规格商品通常只有一个SKU)
  287. $item['sku_id'] = $skus[0]['id'];
  288. // 为每个SKU添加规格属性文本和活动折扣信息
  289. foreach ($skus as &$sku) {
  290. $sku['specs_text'] = $skuSpecTexts[$sku['id']] ?? '';
  291. // 查找该SKU是否有活动折扣数据
  292. $activityKey = $item['id'] . '_' . $sku['id'];
  293. if (isset($activitySkuMap[$activityKey])) {
  294. $activitySku = $activitySkuMap[$activityKey];
  295. $sku['activity_discount'] = $activitySku['discount'];
  296. $sku['activity_discount_price'] = $activitySku['discount_price'];
  297. $sku['activity_stocks'] = $activitySku['stocks'];
  298. }
  299. }
  300. $item['skus'] = $skus;
  301. // 如果是多规格商品,添加规格数据
  302. if ($item['spec_type'] == 1) {
  303. $specs = $specsByGoods[$item['id']] ?? [];
  304. // 按规格分组构建规格数据结构
  305. $spec_groups = [];
  306. foreach ($specs as $spec) {
  307. if (!isset($spec_groups[$spec['spec_id']])) {
  308. $spec_groups[$spec['spec_id']]['id'] = $spec['spec_id'];
  309. $spec_groups[$spec['spec_id']]['name'] = $spec['spec_name'];
  310. $spec_groups[$spec['spec_id']]['type'] = $spec['spec_type'] == 2 ? 'custom' : 'basic';
  311. $spec_groups[$spec['spec_id']]['value'] = [];
  312. }
  313. $spec_groups[$spec['spec_id']]['value'][] = [
  314. 'id' => $spec['spec_value_id'],
  315. 'name' => $spec['value'],
  316. 'image' => $spec['image'] ?: '',
  317. 'description' => $spec['desc'] ?: ''
  318. ];
  319. }
  320. // 转换为数组格式,与前端渲染一致
  321. $item['spec'] = array_values($spec_groups);
  322. // 构建已选择的规格折扣数据(用于前端回显)
  323. $selectedSpecs = [];
  324. $summary = [
  325. 'participate_count' => 0,
  326. 'avg_discount' => 0,
  327. 'total_stocks' => 0
  328. ];
  329. $totalDiscount = 0;
  330. foreach ($skus as $sku) {
  331. $activityKey = $item['id'] . '_' . $sku['id'];
  332. if (isset($activitySkuMap[$activityKey])) {
  333. $activitySku = $activitySkuMap[$activityKey];
  334. $selectedSpecs[] = [
  335. 'sku_id' => $sku['id'],
  336. 'discount' => $activitySku['discount'],
  337. 'discount_price' => $activitySku['discount_price'],
  338. 'discount_stocks' => $activitySku['stocks']
  339. ];
  340. $summary['participate_count']++;
  341. $totalDiscount += floatval($activitySku['discount']);
  342. $summary['total_stocks'] += intval($activitySku['stocks']);
  343. }
  344. }
  345. if ($summary['participate_count'] > 0) {
  346. $summary['avg_discount'] = round($totalDiscount / $summary['participate_count'], 1);
  347. // 为前端JavaScript提供已选择的规格数据
  348. $item['selected_discount_data'] = [
  349. 'goodsId' => $item['id'],
  350. 'spec' => $selectedSpecs,
  351. 'summary' => $summary
  352. ];
  353. }
  354. } else {
  355. // 单规格商品,没有spec数据
  356. $item['spec'] = [];
  357. }
  358. } else {
  359. $item['sku_id'] = 0;
  360. $item['skus'] = [];
  361. $item['spec'] = [];
  362. }
  363. }
  364. }
  365. $this->view->assign('goodsData', $goodsData);
  366. $this->assignconfig('goodsData', $goodsData);
  367. }
  368. $this->view->assign('row', $row);
  369. return $this->view->fetch();
  370. }
  371. $params = $this->request->post('row/a');
  372. if (empty($params)) {
  373. $this->error(__('Parameter %s can not be empty', ''));
  374. }
  375. $params = $this->preExcludeFields($params);
  376. // 处理活动渠道
  377. if (isset($params['channels']) && is_array($params['channels'])) {
  378. $params['channels'] = json_encode($params['channels']);
  379. } else {
  380. $params['channels'] = json_encode([]);
  381. }
  382. // 处理商品ID和商品数量
  383. $goodsIds = isset($params['goods_ids']) ? $params['goods_ids'] : '';
  384. if ($goodsIds) {
  385. $goodsIdArr = explode(',', $goodsIds);
  386. $params['goods_count'] = count($goodsIdArr);
  387. $params['goods_ids'] = json_encode($goodsIdArr);
  388. } else {
  389. $params['goods_count'] = 0;
  390. $params['goods_ids'] = '[]';
  391. }
  392. // // 对于折扣活动,强制设置为指定商品参与
  393. // if ($params['type'] == 'discount') {
  394. // $params['goods_join_type'] = 2;
  395. // }
  396. // 设置活动状态
  397. $now = time();
  398. $startTime = strtotime($params['start_time']);
  399. $endTime = strtotime($params['end_time']);
  400. if ($startTime > $now) {
  401. $params['activity_status'] = 0; // 未开始
  402. } elseif ($startTime <= $now && $endTime > $now) {
  403. $params['activity_status'] = 1; // 进行中
  404. } else {
  405. $params['activity_status'] = 2; // 已结束
  406. }
  407. // 检查是否与其他活动时间冲突(同一时间只能有一个进行中的活动)
  408. $this->checkActivityTimeConflict($startTime, $endTime, $ids);
  409. $result = false;
  410. Db::startTrans();
  411. try {
  412. //是否采用模型验证
  413. if ($this->modelValidate) {
  414. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  415. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
  416. $row->validateFailException()->validate($validate);
  417. }
  418. $result = $row->allowField(true)->save($params);
  419. // 处理折扣数据
  420. // if ($params['type'] == 'discount') {
  421. // 编辑逻辑:智能处理增删改
  422. $result = $this->processActivityGoodsDataForEdit($ids, $params);
  423. if (!$result['success']) {
  424. throw new Exception($result['message']);
  425. }
  426. // }
  427. Db::commit();
  428. } catch (ValidateException|PDOException|Exception $e) {
  429. Db::rollback();
  430. $this->error($e->getMessage());
  431. }
  432. if (false === $result) {
  433. $this->error(__('No rows were updated'));
  434. }
  435. $this->success();
  436. }
  437. /**
  438. * 手动停止活动
  439. */
  440. public function stopActivity()
  441. {
  442. $id = $this->request->post('id');
  443. $activity = $this->model->find($id);
  444. if (!$activity) {
  445. $this->error('活动不存在');
  446. }
  447. if ($activity['activity_status'] != 1) {
  448. $this->error('只有进行中的活动才能手动停止');
  449. }
  450. // 设置停止时间和状态
  451. $activity->stop_time = date('Y-m-d H:i:s');
  452. $activity->activity_status = 3; // 手动停止
  453. if ($activity->save()) {
  454. $this->success('活动已手动停止');
  455. } else {
  456. $this->error('操作失败');
  457. }
  458. }
  459. /**
  460. * 处理活动商品数据
  461. * @param int $activityId 活动ID
  462. * @param array $params 参数数组
  463. * @return array
  464. */
  465. private function processActivityGoodsData($activityId, $params)
  466. {
  467. try {
  468. // 处理商品信息数组
  469. $goodsInfo = isset($params['goods_info']) ? json_decode($params['goods_info'], true) : [];
  470. if (empty($goodsInfo)) {
  471. return ['success' => false, 'message' => '商品信息不能为空'];
  472. }
  473. \think\Log::write('处理商品信息数组: ' . json_encode($goodsInfo), 'debug');
  474. $specsData = [];
  475. foreach ($goodsInfo as $goodsItem) {
  476. if (!isset($goodsItem['goods_id'])) {
  477. continue;
  478. }
  479. $goodsId = $goodsItem['goods_id'];
  480. // 查询商品信息
  481. $goods = Db::name('shop_goods')->where('id', $goodsId)->find();
  482. if (!$goods) {
  483. continue;
  484. }
  485. if ($goodsItem['spec_type'] == 0) {
  486. // 单规格商品处理
  487. if (!isset($goodsItem['discount_price']) || !isset($goodsItem['discount_stocks'])) {
  488. continue;
  489. }
  490. // 获取单规格商品的实际SKU ID
  491. $skuId = isset($goodsItem['sku_id']) ? $goodsItem['sku_id'] : 0;
  492. if ($skuId == 0) {
  493. // 如果没有传SKU ID,查询商品的第一个SKU
  494. $sku = Db::name('shop_goods_sku')->where('goods_id', $goodsId)->find();
  495. $skuId = $sku ? $sku['id'] : 0;
  496. }
  497. $specsData[] = [
  498. 'activity_id' => $activityId,
  499. 'goods_id' => $goodsId,
  500. 'sku_id' => $skuId,
  501. 'discount' => $goodsItem['discount'] ?: round($goodsItem['discount_price'] * 10 / $goods['price'], 1),
  502. 'discount_price' => $goodsItem['discount_price'],
  503. 'stocks' => $goodsItem['discount_stocks'],
  504. 'createtime' => time(),
  505. 'updatetime' => time(),
  506. ];
  507. } else if ($goodsItem['spec_type'] == 1 && isset($goodsItem['spec'])) {
  508. // 多规格商品处理
  509. foreach ($goodsItem['spec'] as $spec) {
  510. if (!isset($spec['sku_id']) || !isset($spec['discount_price']) || !isset($spec['discount_stocks'])) {
  511. continue;
  512. }
  513. // 查询规格原始价格
  514. $skuInfo = Db::name('shop_goods_sku')
  515. ->where(['id' => $spec['sku_id']])
  516. ->find();
  517. if ($skuInfo) {
  518. $specsData[] = [
  519. 'activity_id' => $activityId,
  520. 'goods_id' => $goodsId,
  521. 'sku_id' => $spec['sku_id'],
  522. 'discount' => $spec['discount'] ?: round($spec['discount_price'] * 10 / $skuInfo['price'], 1),
  523. 'discount_price' => $spec['discount_price'],
  524. 'stocks' => $spec['discount_stocks'],
  525. 'createtime' => time(),
  526. 'updatetime' => time(),
  527. ];
  528. }
  529. }
  530. }
  531. }
  532. // 批量插入规格折扣数据
  533. if (!empty($specsData)) {
  534. Db::table('shop_activity_sku')->insertAll($specsData);
  535. \think\Log::write('成功插入' . count($specsData) . '条活动SKU数据', 'debug');
  536. }
  537. return ['success' => true, 'message' => '处理成功'];
  538. } catch (Exception $e) {
  539. \think\Log::write('处理活动商品数据失败: ' . $e->getMessage(), 'error');
  540. return ['success' => false, 'message' => '处理活动商品数据失败: ' . $e->getMessage()];
  541. }
  542. }
  543. /**
  544. * 编辑活动时处理商品数据 - 智能增删改逻辑
  545. * @param int $activityId 活动ID
  546. * @param array $params 提交的参数
  547. * @return array
  548. */
  549. private function processActivityGoodsDataForEdit($activityId, $params)
  550. {
  551. try {
  552. \think\Log::write('开始处理编辑活动商品数据,活动ID: ' . $activityId, 'debug');
  553. // 1. 解析新的商品信息
  554. $newGoodsInfo = json_decode($params['goods_info'], true);
  555. if (!$newGoodsInfo || !is_array($newGoodsInfo)) {
  556. return ['success' => true, 'message' => '没有商品数据需要处理'];
  557. }
  558. // 2. 查询当前活动的所有现有SKU记录
  559. $existingSkus = Db::table('shop_activity_sku')
  560. ->where('activity_id', $activityId)
  561. ->select();
  562. // 构建现有SKU的索引 (goods_id_sku_id => record)
  563. $existingSkuMap = [];
  564. foreach ($existingSkus as $sku) {
  565. $key = $sku['goods_id'] . '_' . $sku['sku_id'];
  566. $existingSkuMap[$key] = $sku;
  567. }
  568. // 3. 构建新提交的SKU数据
  569. $newSkuMap = [];
  570. $newSkuData = [];
  571. foreach ($newGoodsInfo as $goodsInfo) {
  572. $goodsId = $goodsInfo['goods_id'];
  573. if ($goodsInfo['spec_type'] == 0) {
  574. // 单规格商品
  575. $skuId = isset($goodsInfo['sku_id']) && $goodsInfo['sku_id'] > 0 ? $goodsInfo['sku_id'] : 0;
  576. $key = $goodsId . '_' . $skuId;
  577. $newSkuMap[$key] = true;
  578. $newSkuData[$key] = [
  579. 'activity_id' => $activityId,
  580. 'goods_id' => $goodsId,
  581. 'sku_id' => $skuId,
  582. 'discount' => floatval($goodsInfo['discount']),
  583. 'discount_price' => floatval($goodsInfo['discount_price']),
  584. 'stocks' => intval($goodsInfo['discount_stocks']),
  585. 'createtime' => time(),
  586. 'updatetime' => time(),
  587. ];
  588. } elseif ($goodsInfo['spec_type'] == 1 && isset($goodsInfo['spec']) && is_array($goodsInfo['spec'])) {
  589. // 多规格商品
  590. foreach ($goodsInfo['spec'] as $spec) {
  591. $skuId = intval($spec['sku_id']);
  592. $key = $goodsId . '_' . $skuId;
  593. $newSkuMap[$key] = true;
  594. $newSkuData[$key] = [
  595. 'activity_id' => $activityId,
  596. 'goods_id' => $goodsId,
  597. 'sku_id' => $skuId,
  598. 'discount' => floatval($spec['discount']),
  599. 'discount_price' => floatval($spec['discount_price']),
  600. 'stocks' => intval($spec['discount_stocks']),
  601. 'createtime' => time(),
  602. 'updatetime' => time(),
  603. ];
  604. }
  605. }
  606. }
  607. // 4. 分析需要删除、修改、新增的记录
  608. $toDelete = []; // 需要删除的记录ID
  609. $toUpdate = []; // 需要更新的记录
  610. $toInsert = []; // 需要新增的记录
  611. // 找出需要删除的记录(存在于旧数据但不存在于新数据)
  612. foreach ($existingSkuMap as $key => $existingRecord) {
  613. if (!isset($newSkuMap[$key])) {
  614. $toDelete[] = $existingRecord['id'];
  615. }
  616. }
  617. // 找出需要更新和新增的记录
  618. foreach ($newSkuData as $key => $newRecord) {
  619. if (isset($existingSkuMap[$key])) {
  620. // 存在相同的goods_id + sku_id,需要更新
  621. $existingRecord = $existingSkuMap[$key];
  622. // 检查是否有数据变化
  623. $hasChanges =
  624. $existingRecord['discount'] != $newRecord['discount'] ||
  625. $existingRecord['discount_price'] != $newRecord['discount_price'] ||
  626. $existingRecord['stocks'] != $newRecord['stocks'];
  627. if ($hasChanges) {
  628. $toUpdate[] = [
  629. 'id' => $existingRecord['id'],
  630. 'data' => [
  631. 'discount' => $newRecord['discount'],
  632. 'discount_price' => $newRecord['discount_price'],
  633. 'stocks' => $newRecord['stocks'],
  634. 'updatetime' => time(),
  635. ]
  636. ];
  637. }
  638. } else {
  639. // 不存在,需要新增
  640. $toInsert[] = $newRecord;
  641. }
  642. }
  643. // 5. 执行数据库操作
  644. $deleteCount = 0;
  645. $updateCount = 0;
  646. $insertCount = 0;
  647. // 删除操作
  648. if (!empty($toDelete)) {
  649. $deleteCount = Db::table('shop_activity_sku')
  650. ->where('id', 'in', $toDelete)
  651. ->delete();
  652. \think\Log::write('删除了 ' . $deleteCount . ' 条活动SKU记录', 'debug');
  653. }
  654. // 更新操作
  655. foreach ($toUpdate as $updateItem) {
  656. $updated = Db::table('shop_activity_sku')
  657. ->where('id', $updateItem['id'])
  658. ->update($updateItem['data']);
  659. if ($updated) {
  660. $updateCount++;
  661. }
  662. }
  663. if ($updateCount > 0) {
  664. \think\Log::write('更新了 ' . $updateCount . ' 条活动SKU记录', 'debug');
  665. }
  666. // 新增操作
  667. if (!empty($toInsert)) {
  668. Db::table('shop_activity_sku')->insertAll($toInsert);
  669. $insertCount = count($toInsert);
  670. \think\Log::write('新增了 ' . $insertCount . ' 条活动SKU记录', 'debug');
  671. }
  672. $message = "编辑完成:删除 {$deleteCount} 条,更新 {$updateCount} 条,新增 {$insertCount} 条记录";
  673. \think\Log::write($message, 'debug');
  674. return ['success' => true, 'message' => $message];
  675. } catch (Exception $e) {
  676. \think\Log::write('处理编辑活动商品数据失败: ' . $e->getMessage(), 'error');
  677. return ['success' => false, 'message' => '处理编辑活动商品数据失败: ' . $e->getMessage()];
  678. }
  679. }
  680. /**
  681. * 检查活动时间冲突
  682. * @param int $startTime 开始时间戳
  683. * @param int $endTime 结束时间戳
  684. * @param int|null $excludeId 排除的活动ID(编辑时使用)
  685. * @throws Exception
  686. */
  687. private function checkActivityTimeConflict($startTime, $endTime, $excludeId = null)
  688. {
  689. // 构建查询条件
  690. $where = [
  691. 'status' => 1, // 启用状态
  692. 'type' => 'discount', // 折扣活动
  693. ];
  694. // 编辑时排除当前活动
  695. if ($excludeId) {
  696. $where['id'] = ['neq', $excludeId];
  697. }
  698. // 查询可能冲突的活动
  699. $conflictActivities = Db::table('shop_activity')
  700. ->where($where)
  701. ->where(function($query) use ($startTime, $endTime) {
  702. // 检查时间重叠的情况:
  703. // 1. 新活动开始时间在现有活动期间内
  704. // 2. 新活动结束时间在现有活动期间内
  705. // 3. 新活动完全包含现有活动
  706. // 4. 现有活动完全包含新活动
  707. $query->where(function($q) use ($startTime, $endTime) {
  708. // 新活动开始时间在现有活动期间内
  709. $q->where('start_time', '<=', $startTime)
  710. ->where('end_time', '>', $startTime);
  711. })->whereOr(function($q) use ($startTime, $endTime) {
  712. // 新活动结束时间在现有活动期间内
  713. $q->where('start_time', '<', $endTime)
  714. ->where('end_time', '>=', $endTime);
  715. })->whereOr(function($q) use ($startTime, $endTime) {
  716. // 新活动完全包含现有活动
  717. $q->where('start_time', '>=', $startTime)
  718. ->where('end_time', '<=', $endTime);
  719. })->whereOr(function($q) use ($startTime, $endTime) {
  720. // 现有活动完全包含新活动
  721. $q->where('start_time', '<=', $startTime)
  722. ->where('end_time', '>=', $endTime);
  723. });
  724. })
  725. ->where('activity_status', ActivityEnum::ACTIVITY_STATUS_ONGOING)
  726. ->field('id, name, start_time, end_time, activity_status')
  727. ->select();
  728. if (!empty($conflictActivities)) {
  729. $conflictMessages = [];
  730. foreach ($conflictActivities as $activity) {
  731. $startTimeStr = date('Y-m-d H:i:s', $activity['start_time']);
  732. $endTimeStr = date('Y-m-d H:i:s', $activity['end_time']);
  733. $statusText = $this->getActivityStatusText($activity['activity_status']);
  734. $conflictMessages[] = "活动「{$activity['name']}」({$statusText}) 时间:{$startTimeStr} ~ {$endTimeStr}";
  735. }
  736. $errorMessage = "同一时间只能有一个折扣活动,以下活动与新活动时间冲突:\n" . implode("\n", $conflictMessages);
  737. $this->error($errorMessage);
  738. }
  739. }
  740. /**
  741. * 获取活动状态文本
  742. * @param int $status
  743. * @return string
  744. */
  745. private function getActivityStatusText($status)
  746. {
  747. $statusMap = [
  748. 0 => '未开始',
  749. 1 => '进行中',
  750. 2 => '已结束',
  751. 3 => '手动停止'
  752. ];
  753. return $statusMap[$status] ?? '未知状态';
  754. }
  755. }