ShopTemplateClass.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. <?php
  2. /**
  3. * 零售店开店样例模板复制
  4. * 谁用:SjService::initHdBaseInfo 在申请勾选「生成样例模板」后调用
  5. * 解决问题:把指定模板店的首页配置、分类、使用场景、商品、秒杀数据复制给新开零售店,并重写关联 ID
  6. */
  7. namespace bizHd\shop\classes;
  8. use bizHd\base\classes\BaseClass;
  9. use bizHd\goods\classes\CategoryClass;
  10. use bizHd\goods\classes\GoodsCategoryClass;
  11. use bizHd\goods\classes\GoodsClass;
  12. use bizHd\goods\classes\GoodsUseCaseClass;
  13. use bizHd\goods\classes\KindClass;
  14. use bizHd\goods\classes\UseCaseClass;
  15. use bizHd\homePageConfig\classes\HomePageConfigClass;
  16. use bizHd\homePageConfig\classes\HomePageModuleConfigClass;
  17. use bizHd\seckill\classes\SeckillActivityClass;
  18. use bizHd\seckill\classes\SeckillGoodsClass;
  19. use Yii;
  20. class ShopTemplateClass extends BaseClass
  21. {
  22. /**
  23. * 从模板店复制零售店样例数据到新店
  24. * @param int $tplMainId 模板店 mainId(按环境写死)
  25. * @param int $tplShopId 模板店 shopId(按环境写死)
  26. * @param object $shopInfo 新创建的门店对象,需含 id/mainId/sjId
  27. * @return bool
  28. */
  29. public static function copyTemplateData($tplMainId, $tplShopId, $shopInfo)
  30. {
  31. $tplMainId = intval($tplMainId);
  32. $tplShopId = intval($tplShopId);
  33. $newMainId = intval($shopInfo->mainId ?? 0);
  34. $newShopId = intval($shopInfo->id ?? 0);
  35. $newSjId = intval($shopInfo->sjId ?? 0);
  36. if ($tplMainId <= 0 || $tplShopId <= 0 || $newMainId <= 0 || $newShopId <= 0) {
  37. Yii::error('复制零售店样例模板参数无效');
  38. return false;
  39. }
  40. $newShop = [
  41. 'mainId' => $newMainId,
  42. 'shopId' => $newShopId,
  43. 'sjId' => $newSjId,
  44. ];
  45. // 已有开店事务时 Yii2 会创建保存点;独立调用时则开启新事务。
  46. // 任一步骤失败都只回滚模板数据,避免外层继续提交残缺的分类、商品或首页配置。
  47. $transaction = Yii::$app->db->beginTransaction();
  48. try {
  49. // 分类必须先复制:商品分类关联、首页金刚区/轮播都依赖分类 ID 映射
  50. $categoryMap = self::copyCategories($tplMainId, $newShop);
  51. $useCaseMap = self::copyUseCases($tplMainId, $tplShopId, $newShop);
  52. $kindMap = self::buildKindMap($tplMainId, $newMainId);
  53. $goodsMap = self::copyGoods($tplMainId, $tplShopId, $newShop, $kindMap);
  54. self::copyGoodsCategories($tplMainId, $newShop, $goodsMap, $categoryMap);
  55. self::copyUseCaseRelations($tplMainId, $newShop, $goodsMap, $useCaseMap);
  56. $activityMap = self::copySeckillActivities($tplMainId, $tplShopId, $newShop);
  57. self::copySeckillGoods($tplMainId, $tplShopId, $newShop, $activityMap, $goodsMap);
  58. self::copyHomePageConfig($tplShopId, $newShop, $goodsMap, $categoryMap, $useCaseMap);
  59. $transaction->commit();
  60. return true;
  61. } catch (\Throwable $e) {
  62. $transaction->rollBack();
  63. throw $e;
  64. }
  65. }
  66. /**
  67. * 删掉开店时初始化的默认分类,再整份复制模板店分类
  68. * 为什么先删:ShopClass::addShop 已写入花束/花篮/永生花/绿植,勾选模板后以模板店分类为准
  69. * @param int $tplMainId
  70. * @param array $newShop
  71. * @return array 旧分类id => 新分类id
  72. */
  73. protected static function copyCategories($tplMainId, $newShop)
  74. {
  75. $map = [];
  76. CategoryClass::deleteByCondition(['mainId' => $newShop['mainId']]);
  77. $list = CategoryClass::getAllByCondition(['mainId' => $tplMainId, 'delStatus' => 0], 'id ASC');
  78. if (empty($list)) {
  79. return $map;
  80. }
  81. foreach ($list as $row) {
  82. $oldId = intval($row['id'] ?? 0);
  83. $payload = self::prepareCopyRow($row, $newShop, ['goodsNum']);
  84. $payload['goodsNum'] = 0;
  85. $created = CategoryClass::add($payload, true);
  86. $newId = intval($created->id ?? 0);
  87. if ($oldId > 0 && $newId > 0) {
  88. $map[$oldId] = $newId;
  89. }
  90. }
  91. return $map;
  92. }
  93. /**
  94. * 复制使用场景,建立旧 useCaseId => 新 useCaseId 映射
  95. * @param int $tplMainId
  96. * @param int $tplShopId
  97. * @param array $newShop
  98. * @return array
  99. */
  100. protected static function copyUseCases($tplMainId, $tplShopId, $newShop)
  101. {
  102. $map = [];
  103. $list = UseCaseClass::getAllByCondition([
  104. 'mainId' => $tplMainId,
  105. 'shopId' => $tplShopId,
  106. 'delStatus' => 0,
  107. ], 'inTurn DESC, id ASC');
  108. if (empty($list)) {
  109. return $map;
  110. }
  111. foreach ($list as $row) {
  112. $oldId = intval($row['id'] ?? 0);
  113. $payload = self::prepareCopyRow($row, $newShop);
  114. $created = UseCaseClass::add($payload, true);
  115. $newId = intval($created->id ?? 0);
  116. if ($oldId > 0 && $newId > 0) {
  117. $map[$oldId] = $newId;
  118. }
  119. }
  120. return $map;
  121. }
  122. /**
  123. * 按种类名称把模板店 kindId 映射到新店已初始化的种类
  124. * 为什么按名称:开店时 KindClass::initKind 已给新店写过一套种类,不能直接复用模板店 kindId
  125. * @param int $tplMainId
  126. * @param int $newMainId
  127. * @return array 旧kindId => 新kindId
  128. */
  129. protected static function buildKindMap($tplMainId, $newMainId)
  130. {
  131. $map = [];
  132. $oldList = KindClass::getAllByCondition(['mainId' => $tplMainId, 'delStatus' => 0], null, 'id,name');
  133. $newList = KindClass::getAllByCondition(['mainId' => $newMainId, 'delStatus' => 0], null, 'id,name');
  134. $newByName = [];
  135. foreach ($newList as $item) {
  136. $name = strval($item['name'] ?? '');
  137. if ($name !== '') {
  138. $newByName[$name] = intval($item['id']);
  139. }
  140. }
  141. foreach ($oldList as $item) {
  142. $name = strval($item['name'] ?? '');
  143. $oldId = intval($item['id'] ?? 0);
  144. if ($oldId > 0 && isset($newByName[$name])) {
  145. $map[$oldId] = $newByName[$name];
  146. }
  147. }
  148. return $map;
  149. }
  150. /**
  151. * 复制商品:先主记录再子规格,销量清零,masterId/kindId 按映射重写
  152. * 商品按 mainId 归属,xhGoods 没有 shopId,不能按门店过滤
  153. * @param int $tplMainId
  154. * @param int $tplShopId
  155. * @param array $newShop
  156. * @param array $kindMap
  157. * @return array 旧goodsId => 新goodsId
  158. */
  159. protected static function copyGoods($tplMainId, $tplShopId, $newShop, $kindMap)
  160. {
  161. $map = [];
  162. $list = GoodsClass::getAllByCondition([
  163. 'mainId' => $tplMainId,
  164. 'delStatus' => 0,
  165. ], 'id ASC');
  166. if (empty($list)) {
  167. return $map;
  168. }
  169. $masters = [];
  170. $children = [];
  171. foreach ($list as $row) {
  172. if (intval($row['masterId'] ?? 0) === 0) {
  173. $masters[] = $row;
  174. } else {
  175. $children[] = $row;
  176. }
  177. }
  178. foreach ($masters as $row) {
  179. $oldId = intval($row['id'] ?? 0);
  180. $payload = self::prepareGoodsRow($row, $newShop, $kindMap, 0);
  181. $created = GoodsClass::add($payload, true);
  182. $newId = intval($created->id ?? 0);
  183. if ($oldId > 0 && $newId > 0) {
  184. $map[$oldId] = $newId;
  185. }
  186. }
  187. foreach ($children as $row) {
  188. $oldId = intval($row['id'] ?? 0);
  189. $oldMasterId = intval($row['masterId'] ?? 0);
  190. if (!isset($map[$oldMasterId])) {
  191. continue;
  192. }
  193. $payload = self::prepareGoodsRow($row, $newShop, $kindMap, $map[$oldMasterId]);
  194. $created = GoodsClass::add($payload, true);
  195. $newId = intval($created->id ?? 0);
  196. if ($oldId > 0 && $newId > 0) {
  197. $map[$oldId] = $newId;
  198. }
  199. }
  200. return $map;
  201. }
  202. /**
  203. * 整理商品复制字段:归属改到新店,销量清零,种类指向新店 kind
  204. * @param array $row
  205. * @param array $newShop
  206. * @param array $kindMap
  207. * @param int $newMasterId
  208. * @return array
  209. */
  210. protected static function prepareGoodsRow($row, $newShop, $kindMap, $newMasterId)
  211. {
  212. $payload = self::prepareCopyRow($row, $newShop);
  213. $payload['masterId'] = intval($newMasterId);
  214. $payload['sold'] = 0;
  215. $payload['actualSold'] = 0;
  216. $payload['createTime'] = date('Y-m-d H:i:s');
  217. $oldKindId = intval($row['kindId'] ?? 0);
  218. if ($oldKindId > 0) {
  219. $payload['kindId'] = $kindMap[$oldKindId] ?? 0;
  220. }
  221. return $payload;
  222. }
  223. /**
  224. * 按商品/分类映射复制 xhGoodsCategory,并回写分类商品数量
  225. * @param int $tplMainId
  226. * @param array $newShop
  227. * @param array $goodsMap
  228. * @param array $categoryMap
  229. * @return void
  230. */
  231. protected static function copyGoodsCategories($tplMainId, $newShop, $goodsMap, $categoryMap)
  232. {
  233. if (empty($goodsMap) || empty($categoryMap)) {
  234. return;
  235. }
  236. $oldGoodsIds = array_keys($goodsMap);
  237. $list = GoodsCategoryClass::getAllByCondition([
  238. 'mainId' => $tplMainId,
  239. 'gId' => ['in', $oldGoodsIds],
  240. ], 'id ASC');
  241. if (empty($list)) {
  242. return;
  243. }
  244. $goodsNum = [];
  245. foreach ($list as $row) {
  246. $oldGoodsId = intval($row['gId'] ?? 0);
  247. $oldCatId = intval($row['cId'] ?? 0);
  248. if (!isset($goodsMap[$oldGoodsId]) || !isset($categoryMap[$oldCatId])) {
  249. continue;
  250. }
  251. $newCatId = $categoryMap[$oldCatId];
  252. $payload = self::prepareCopyRow($row, $newShop);
  253. $payload['gId'] = $goodsMap[$oldGoodsId];
  254. $payload['cId'] = $newCatId;
  255. GoodsCategoryClass::add($payload);
  256. $goodsNum[$newCatId] = ($goodsNum[$newCatId] ?? 0) + 1;
  257. }
  258. foreach ($goodsNum as $catId => $num) {
  259. CategoryClass::updateById($catId, ['goodsNum' => $num]);
  260. }
  261. }
  262. /**
  263. * 按商品/场景映射复制 xhGoodsUseCaseRelation
  264. * @param int $tplMainId
  265. * @param array $newShop
  266. * @param array $goodsMap
  267. * @param array $useCaseMap
  268. * @return void
  269. */
  270. protected static function copyUseCaseRelations($tplMainId, $newShop, $goodsMap, $useCaseMap)
  271. {
  272. if (empty($goodsMap) || empty($useCaseMap)) {
  273. return;
  274. }
  275. $oldGoodsIds = array_keys($goodsMap);
  276. $list = GoodsUseCaseClass::getAllByCondition([
  277. 'mainId' => $tplMainId,
  278. 'goodsId' => ['in', $oldGoodsIds],
  279. ], 'id ASC');
  280. if (empty($list)) {
  281. return;
  282. }
  283. foreach ($list as $row) {
  284. $oldGoodsId = intval($row['goodsId'] ?? 0);
  285. $oldUseCaseId = intval($row['useCaseId'] ?? 0);
  286. if (!isset($goodsMap[$oldGoodsId]) || !isset($useCaseMap[$oldUseCaseId])) {
  287. continue;
  288. }
  289. GoodsUseCaseClass::add([
  290. 'mainId' => $newShop['mainId'],
  291. 'goodsId' => $goodsMap[$oldGoodsId],
  292. 'useCaseId' => $useCaseMap[$oldUseCaseId],
  293. ]);
  294. }
  295. }
  296. /**
  297. * 复制模板店未删除的秒杀活动批次(秒杀按 mainId 共享,不按 shopId 过滤)
  298. * @param int $tplMainId
  299. * @param int $tplShopId
  300. * @param array $newShop
  301. * @return array 旧activityId => 新activityId
  302. */
  303. protected static function copySeckillActivities($tplMainId, $tplShopId, $newShop)
  304. {
  305. $map = [];
  306. $list = SeckillActivityClass::getAllByCondition([
  307. 'mainId' => $tplMainId,
  308. 'delStatus' => 0,
  309. ], 'id ASC');
  310. if (empty($list)) {
  311. return $map;
  312. }
  313. foreach ($list as $row) {
  314. $oldId = intval($row['id'] ?? 0);
  315. $payload = self::prepareCopyRow($row, $newShop);
  316. $created = SeckillActivityClass::add($payload, true);
  317. $newId = intval($created->id ?? 0);
  318. if ($oldId > 0 && $newId > 0) {
  319. $map[$oldId] = $newId;
  320. }
  321. }
  322. return $map;
  323. }
  324. /**
  325. * 复制秒杀商品版本,重写 activityId 与 goodsId
  326. * @param int $tplMainId
  327. * @param int $tplShopId
  328. * @param array $newShop
  329. * @param array $activityMap
  330. * @param array $goodsMap
  331. * @return void
  332. */
  333. protected static function copySeckillGoods($tplMainId, $tplShopId, $newShop, $activityMap, $goodsMap)
  334. {
  335. if (empty($activityMap) || empty($goodsMap)) {
  336. return;
  337. }
  338. $list = SeckillGoodsClass::getAllByCondition([
  339. 'mainId' => $tplMainId,
  340. 'delStatus' => 0,
  341. ], 'id ASC');
  342. if (empty($list)) {
  343. return;
  344. }
  345. foreach ($list as $row) {
  346. $oldActivityId = intval($row['activityId'] ?? 0);
  347. $oldGoodsId = intval($row['goodsId'] ?? 0);
  348. if (!isset($activityMap[$oldActivityId]) || !isset($goodsMap[$oldGoodsId])) {
  349. continue;
  350. }
  351. $payload = self::prepareCopyRow($row, $newShop);
  352. $payload['activityId'] = $activityMap[$oldActivityId];
  353. $payload['goodsId'] = $goodsMap[$oldGoodsId];
  354. SeckillGoodsClass::add($payload);
  355. }
  356. }
  357. /**
  358. * 复制首页模块配置,并把 JSON 里引用的商品/分类/场景 ID 换成新店 ID
  359. * @param int $tplShopId
  360. * @param array $newShop
  361. * @param array $goodsMap
  362. * @param array $categoryMap
  363. * @param array $useCaseMap
  364. * @return void
  365. */
  366. protected static function copyHomePageConfig($tplShopId, $newShop, $goodsMap, $categoryMap, $useCaseMap)
  367. {
  368. $list = HomePageModuleConfigClass::getAllByCondition(['shopId' => $tplShopId], 'id ASC');
  369. if (empty($list)) {
  370. return;
  371. }
  372. foreach ($list as $row) {
  373. $moduleKey = strval($row['moduleKey'] ?? '');
  374. if ($moduleKey === '') {
  375. continue;
  376. }
  377. $payload = json_decode($row['configValue'] ?? '', true);
  378. if (!is_array($payload)) {
  379. $payload = [];
  380. }
  381. $payload = self::remapHomePagePayload($moduleKey, $payload, $goodsMap, $categoryMap, $useCaseMap);
  382. HomePageModuleConfigClass::saveConfigValue($newShop['mainId'], $newShop['shopId'], $moduleKey, $payload);
  383. }
  384. }
  385. /**
  386. * 按 moduleKey 把首页配置里的关联 ID 换成新店数据
  387. * banner/navGrid 的 list[].type+value、hot/new/pullGoods 的 type+value 都可能指向商品/分类/场景
  388. * @param string $moduleKey
  389. * @param array $payload
  390. * @param array $goodsMap
  391. * @param array $categoryMap
  392. * @param array $useCaseMap
  393. * @return array
  394. */
  395. protected static function remapHomePagePayload($moduleKey, $payload, $goodsMap, $categoryMap, $useCaseMap)
  396. {
  397. if ($moduleKey === 'banner' || $moduleKey === 'navGrid') {
  398. if (empty($payload['list']) || !is_array($payload['list'])) {
  399. return $payload;
  400. }
  401. foreach ($payload['list'] as $index => $item) {
  402. if (!is_array($item)) {
  403. continue;
  404. }
  405. $type = intval($item['type'] ?? 0);
  406. $value = strval($item['value'] ?? '');
  407. $payload['list'][$index]['value'] = self::remapLinkedValue($type, $value, $goodsMap, $categoryMap, $useCaseMap);
  408. }
  409. return $payload;
  410. }
  411. if (in_array($moduleKey, ['hot', 'new', 'pullGoods'], true)) {
  412. $type = intval($payload['type'] ?? 0);
  413. $value = strval($payload['value'] ?? '');
  414. $payload['value'] = self::remapLinkedValue($type, $value, $goodsMap, $categoryMap, $useCaseMap);
  415. return $payload;
  416. }
  417. return $payload;
  418. }
  419. /**
  420. * 把关联配置的 value(单个 ID 或逗号拼接)按 type 映射到新店
  421. * type:2 商品、3 分类、4 使用场景;未映射到的 ID 丢弃,避免新店引用模板店数据
  422. * @param int $type
  423. * @param string $value
  424. * @param array $goodsMap
  425. * @param array $categoryMap
  426. * @param array $useCaseMap
  427. * @return string
  428. */
  429. protected static function remapLinkedValue($type, $value, $goodsMap, $categoryMap, $useCaseMap)
  430. {
  431. $map = [];
  432. if ($type === HomePageConfigClass::BANNER_TYPE_GOODS) {
  433. $map = $goodsMap;
  434. } elseif ($type === HomePageConfigClass::BANNER_TYPE_CATEGORY) {
  435. $map = $categoryMap;
  436. } elseif ($type === HomePageConfigClass::BANNER_TYPE_USE_CASE) {
  437. $map = $useCaseMap;
  438. } else {
  439. return $value;
  440. }
  441. $ids = array_filter(array_map('intval', explode(',', strval($value))));
  442. $mapped = [];
  443. foreach ($ids as $id) {
  444. if (isset($map[$id])) {
  445. $mapped[] = $map[$id];
  446. }
  447. }
  448. return implode(',', $mapped);
  449. }
  450. /**
  451. * 去掉主键与时间戳,并把归属字段改成新店
  452. * @param array $row
  453. * @param array $newShop
  454. * @param array $extraUnset 额外要丢掉的字段
  455. * @return array
  456. */
  457. protected static function prepareCopyRow($row, $newShop, $extraUnset = [])
  458. {
  459. unset($row['id'], $row['addTime'], $row['updateTime']);
  460. foreach ($extraUnset as $field) {
  461. unset($row[$field]);
  462. }
  463. $row['mainId'] = $newShop['mainId'];
  464. if (array_key_exists('shopId', $row)) {
  465. $row['shopId'] = $newShop['shopId'];
  466. }
  467. if (array_key_exists('sjId', $row)) {
  468. $row['sjId'] = $newShop['sjId'];
  469. }
  470. return $row;
  471. }
  472. }