WiwibaoProductSync.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. <?php
  2. declare (strict_types=1);
  3. namespace app\command;
  4. use app\common\enum\CommonEnum;
  5. use app\common\enum\douhuomall\DouhuomallEnum;
  6. use app\common\enum\store\ProductEnum;
  7. use app\common\repositories\store\product\ProductRepository;
  8. use app\traits\GongApiRequest;
  9. use think\console\Command;
  10. use think\console\Input;
  11. use think\console\Output;
  12. use think\db\exception\DataNotFoundException;
  13. use think\db\exception\DbException;
  14. use think\db\exception\ModelNotFoundException;
  15. use think\facade\Db;
  16. use think\facade\Log;
  17. class WiwibaoProductSync extends Command
  18. {
  19. use GongApiRequest;
  20. // 配置参数
  21. protected $config = [
  22. 'page_size' => 100, // 每页拉取数量
  23. 'max_pages' => 0, // 最大页数限制
  24. 'price_change_rate' => 0.0, // 价格变动比率阈值(10%)
  25. 'mer_id' => CommonEnum::DESIGN_MERCHANT_ID['WonderfulLiving']['code']
  26. ];
  27. protected function configure()
  28. {
  29. // 指令配置
  30. $this->setName('wiwibaoproductsync')
  31. ->setDescription('the wiwibaoproductsync command');
  32. }
  33. protected function execute(Input $input, Output $output)
  34. {
  35. $output->writeln('定时任务执行中...');
  36. $this->syncGoodsFromGong();
  37. $output->writeln('定时任务执行完成');
  38. }
  39. /**
  40. * 同步微唯宝商品数据(主方法)
  41. */
  42. public function syncGoodsFromGong()
  43. {
  44. try {
  45. set_time_limit(0);
  46. // 1. 批量拉取所有商品数据
  47. $allProducts = $this->fetchAllProducts();
  48. if (empty($allProducts)) {
  49. Log::info('未拉取到任何商品数据');
  50. return 'success';
  51. }
  52. // 2. 获取本地所有SPU ID
  53. $douhuomallDataMap = [];
  54. $localSpuIds = [];
  55. $douhuomallDataList = Db::name('douhuomall')
  56. ->select()
  57. ->toArray();
  58. if (!empty($douhuomallDataList)) {
  59. $douhuomallDataMap = array_column($douhuomallDataList, null, 'spu_id');
  60. $localSpuIds = array_keys($douhuomallDataMap);
  61. }
  62. $syncedSpuIds = [];
  63. // 3. 批量处理商品数据
  64. foreach ($allProducts as $product) {
  65. try {
  66. $spuId = $this->processSingleProduct($product, $douhuomallDataMap[$product['goods_id']] ?? []);
  67. if ($spuId) {
  68. $syncedSpuIds[] = $spuId;
  69. }
  70. } catch (\Exception $e) {
  71. Log::error("处理商品 {$product['goods_id']} 失败: " . $e->getMessage());
  72. continue;
  73. }
  74. }
  75. // 4. 处理需要下架的商品(本地有但第三方已下架)
  76. $this->handleOfflineProducts($localSpuIds, $syncedSpuIds);
  77. return 'success';
  78. } catch (\Exception $e) {
  79. $this->logError('商品同步失败', $e);
  80. return 'error';
  81. }
  82. }
  83. /**
  84. * 批量拉取所有商品数据
  85. */
  86. protected function fetchAllProducts()
  87. {
  88. $allData = [];
  89. // 第一页
  90. $firstPageData = GongApiRequest::get_goods_list(1, $this->config['page_size']);
  91. if (empty($firstPageData['list'])) {
  92. return [];
  93. }
  94. $allData = $firstPageData['list'];
  95. $totalPages = $firstPageData['pages'] ?? 1;
  96. if (!empty($this->config['max_pages'])) {
  97. $totalPages = min($totalPages, $this->config['max_pages']);
  98. }
  99. // 并行获取剩余页面
  100. for ($page = 2; $page <= $totalPages; $page++) {
  101. $pageData = GongApiRequest::get_goods_list($page, $this->config['page_size']);
  102. if (!empty($pageData['list'])) {
  103. $allData = array_merge($allData, $pageData['list']);
  104. }
  105. }
  106. return $allData;
  107. }
  108. /**
  109. * 处理单个商品
  110. */
  111. protected function processSingleProduct($productData, $douhuomallData)
  112. {
  113. $goodsId = $productData['goods_id'];
  114. // 1. 获取商品详情
  115. $goodsInfo = GongApiRequest::get_goods_info($goodsId);
  116. if (!$goodsInfo) {
  117. return null;
  118. }
  119. // 2. 处理图片信息
  120. $imageData = $this->processProductImages($goodsId, $goodsInfo);
  121. // 3. 处理SKU信息
  122. $skuInfo = $this->processSkuInfo($goodsInfo['sku_list'] ?? [], $goodsId);
  123. // 4. 构建保存数据
  124. $saveData = $this->buildSaveData($goodsInfo, $skuInfo, $imageData);
  125. // 5. 保存到本地(使用原有的add方法逻辑)
  126. $spuId = $this->addToDouhuomall($saveData, $douhuomallData);
  127. // 6. 检查并处理已上架商品的价格变动
  128. $this->checkAndProcessOnlineProduct($spuId, $saveData);
  129. return $spuId;
  130. }
  131. /**
  132. * 处理商品图片信息
  133. */
  134. protected function processProductImages($goodsId, $goodsInfo)
  135. {
  136. // 如果你需要保存图片到本地,可以在这里实现
  137. // 否则直接返回URL
  138. return [
  139. 'cover_url' => $goodsInfo['main_img'] ?? '',
  140. 'detail_images' => $goodsInfo['detail_img'] ?? [],
  141. 'detail_img_list' => $goodsInfo['detail_img'] ?? [] // 为了兼容以前写的代码,建议保留这个数据项,之前用的是detail_img_list字段,但我感觉他们意义一样
  142. ];
  143. }
  144. /**
  145. * 处理SKU信息
  146. */
  147. protected function processSkuInfo($skuList, $goodsId)
  148. {
  149. if (empty($skuList)) {
  150. return [];
  151. }
  152. $processedSkus = [];
  153. foreach ($skuList as $sku) {
  154. if (empty($sku['plat_price'])) {
  155. continue;
  156. }
  157. $processedSku = [
  158. 'sku_id' => $sku['sku_id'] ?? 0,
  159. 'plat_price' => $sku['plat_price'],
  160. 'img_url' => $sku['main_img'],
  161. 'retail_price' => $sku['retail_price'] ?? 0,
  162. 'cost_price' => $this->calculateCostPrice($sku['plat_price']),
  163. 'market_price' => $this->calculateMarketPrice($sku['plat_price']),
  164. 'profit' => $this->calculateProfit($sku['plat_price']),
  165. 'mer_profit' => $this->calculateMerProfit($sku['plat_price']),
  166. 'yanglaojin' => '0.00',
  167. 'main_img' => $sku['main_img'] ?? '',
  168. 'attribute_json' => !empty($sku['attr']) ? $sku['attr'] : [['val' => '默认', 'name' => '默认']]
  169. ];
  170. $processedSkus[] = array_merge($sku, $processedSku);
  171. }
  172. return $processedSkus;
  173. }
  174. /**
  175. * 构建保存数据
  176. */
  177. protected function buildSaveData($goodsInfo, $skuInfo, $imageData)
  178. {
  179. // 构建详情HTML
  180. $detailHtml = $this->buildDetailHtml($imageData['detail_images']);
  181. return [
  182. 'spu_id' => $goodsInfo['goods_id'],
  183. 'skuId_info' => $skuInfo,
  184. 'sys_id' => '112',
  185. 'goods_id' => $goodsInfo['goods_id'],
  186. 'spuId_info' => array_merge(
  187. $goodsInfo,
  188. [
  189. 'cover_url' => $imageData['cover_url'],
  190. 'detail' => $detailHtml,
  191. 'detail_img_list' => $imageData['detail_img_list']
  192. ]
  193. ),
  194. 'sku_id' => $skuInfo[0]['sku_id'] ?? 0,
  195. 'status' => $goodsInfo['status'] ?? 0,
  196. 'cate_ids' => $goodsInfo['cate_ids'] ?? '',
  197. 'title' => $goodsInfo['spu_name'] ?? '',
  198. 'cxb_cate_id' => 0
  199. ];
  200. }
  201. /**
  202. * 构建详情HTML
  203. */
  204. protected function buildDetailHtml($detailImages)
  205. {
  206. if (empty($detailImages)) {
  207. return '';
  208. }
  209. $detailHtml = '';
  210. foreach ((array)$detailImages as $imgUrl) {
  211. $detailHtml .= "<img src='{$imgUrl}' referrerpolicy='no-referrer' />";
  212. }
  213. return $detailHtml;
  214. }
  215. /**
  216. * @param $data
  217. * @param $findData
  218. * @return mixed
  219. * @throws DbException
  220. */
  221. protected function addToDouhuomall($data, $findData)
  222. {
  223. try {
  224. // 对SKU按成本价排序,获取最低成本价的SKU
  225. $skuInfo = $this->arrSort($data['skuId_info'], 'cost_price', SORT_ASC);
  226. // 计算价格字段
  227. $data['cost_price'] = $skuInfo[0]['cost_price'] ?? 0;
  228. $data['market_price'] = $skuInfo[0]['market_price'] ?? 0;
  229. $data['ot_price'] = $skuInfo[0]['retail_price'] ?? 0;
  230. $data['mer_profit'] = $skuInfo[0]['mer_profit'] ?? 0;
  231. $data['pension'] = $skuInfo[0]['yanglaojin'] ?? 0;
  232. $data['profit'] = (empty($data['cost_price']) || empty($data['market_price']))
  233. ? 0
  234. : bcdiv((string)$data['market_price'], (string)$data['cost_price'], 2);
  235. $now = date('Y-m-d H:i:s');
  236. if (empty($findData)) {
  237. // 新增
  238. $insertData = [
  239. 'spu_id' => $data['spu_id'],
  240. 'skuId_info' => json_encode($data['skuId_info'], JSON_UNESCAPED_UNICODE),
  241. 'sys_id' => $data['sys_id'],
  242. 'goods_id' => $data['goods_id'],
  243. 'spuId_info' => json_encode($data['spuId_info'], JSON_UNESCAPED_UNICODE),
  244. 'create_time' => $now,
  245. 'update_time' => $now,
  246. 'sku_id' => $data['sku_id'],
  247. 'status' => $data['status'],
  248. 'cate_ids' => $data['cate_ids'],
  249. 'title' => $data['title'],
  250. 'cost_price' => $data['cost_price'],
  251. 'market_price' => $data['market_price'],
  252. 'ot_price' => $data['ot_price'],
  253. 'profit' => $data['profit'],
  254. 'mer_profit' => $data['mer_profit'],
  255. 'pension' => $data['pension'],
  256. 'cxb_cate_id' => $data['cxb_cate_id']
  257. ];
  258. Db::name('douhuomall')->insert($insertData);
  259. } else {
  260. // 更新
  261. // 无意义 // 检查是否需要恢复已删除商品的上架状态
  262. // $shouldRestore = $this->shouldRestoreProduct($findData, $data);
  263. // if ($shouldRestore) {
  264. // $this->restoreProduct($data['spu_id']);
  265. // }
  266. $updateData = [
  267. 'status' => $data['status'],
  268. 'update_time' => $now,
  269. 'cost_price' => $data['cost_price'],
  270. 'market_price' => $data['market_price'],
  271. 'ot_price' => $data['ot_price'],
  272. 'mer_profit' => $data['mer_profit'],
  273. 'pension' => $data['pension'],
  274. 'profit' => $data['profit'],
  275. 'skuId_info' => json_encode($data['skuId_info'], JSON_UNESCAPED_UNICODE),
  276. 'spuId_info' => json_encode($data['spuId_info'], JSON_UNESCAPED_UNICODE),
  277. 'title' => $data['title'],
  278. 'cate_ids' => $data['cate_ids'],
  279. 'cxb_cate_id' => $data['cxb_cate_id']
  280. ];
  281. Db::name('douhuomall')
  282. ->where('spu_id', $findData['spu_id'])
  283. ->update($updateData);
  284. }
  285. return $data['spu_id'];
  286. } catch (\Exception $e) {
  287. Log::error("保存商品到douhuomall失败: " . $e->getMessage());
  288. throw $e;
  289. }
  290. }
  291. /**
  292. * 检查是否需要恢复已删除商品的上架状态
  293. */
  294. protected function shouldRestoreProduct($findData, $newData)
  295. {
  296. // 原有逻辑:如果货物属于已被删除状态,并且新更新进来的状态非删除状态
  297. // 并且成本价、零售价、市场价、利润率没有变动的话
  298. if ($findData['status'] == DouhuomallEnum::STATUS['Delete']['code']
  299. && $newData['status'] !== DouhuomallEnum::STATUS['Delete']['code']
  300. && $newData['cost_price'] == $findData['cost_price']
  301. && $newData['ot_price'] == $findData['ot_price']
  302. && $newData['market_price'] == $findData['market_price']
  303. && $newData['profit'] == $findData['profit']) {
  304. // 检查是否有已下架的商品
  305. $count = Db::name('store_product')
  306. ->where('spu_id', $newData['spu_id'])
  307. ->where('is_show', ProductEnum::IS_SHOW['No']['code'])
  308. ->where('is_status', ProductEnum::IS_STATUS['Automatic']['code'])
  309. ->count();
  310. return $count > 0;
  311. }
  312. return false;
  313. }
  314. /**
  315. * 恢复商品上架状态
  316. */
  317. protected function restoreProduct($spuId)
  318. {
  319. Db::name('store_product')
  320. ->where('spu_id', $spuId)
  321. ->update([
  322. 'is_show' => ProductEnum::IS_SHOW['Yes']['code'],
  323. 'status' => ProductEnum::STATUS['Approved']['code']
  324. ]);
  325. Log::info("恢复商品上架(恢复到仓库中)状态: spu_id={$spuId}");
  326. }
  327. /**
  328. * 检查并处理已上架商品
  329. */
  330. protected function checkAndProcessOnlineProduct($spuId, $syncData)
  331. {
  332. // 检查商品是否已上架
  333. try {
  334. $productList = Db::name('store_product')
  335. // ->where('is_show', ProductEnum::IS_SHOW['Yes']['code'])
  336. ->where('spu_id', $spuId)
  337. ->select()
  338. ->toArray();
  339. if (empty($productList)) {
  340. // 如果没有该商品则自动上架
  341. $this->insert_product($spuId, $this->config['mer_id']);
  342. return;
  343. }
  344. // 解析同步数据中的价格
  345. $syncPrices = [];
  346. foreach ($syncData['skuId_info'] as $sku) {
  347. $syncPrices[] = [
  348. 'sku_id' => $sku['sku_id'] ?? '',
  349. 'market_price' => $sku['market_price'] ?? 0,
  350. 'cost_price' => $sku['cost_price'] ?? 0,
  351. 'retail_price' => $sku['retail_price'] ?? 0
  352. ];
  353. }
  354. foreach ($productList as $product) {
  355. if ($product['is_show'] == ProductEnum::IS_SHOW['No']['code'] && $product['is_status'] == ProductEnum::IS_STATUS['Triggered']['code']) {
  356. // 如果商品 属于未上架商品(或是被员工下架了) 则不做任何处理
  357. continue;
  358. }
  359. // 获取本地商品价格信息
  360. // 获取SKU价格
  361. $skus = Db::name('store_product_attr_value')
  362. ->where('product_id', $product['product_id'])
  363. ->field('gong_sku_id, sku, price, cost')
  364. ->select()
  365. ->toArray();
  366. $localPrices = [
  367. 'product_price' => $product['price'] ?? 0,
  368. 'product_cost' => $product['cost'] ?? 0,
  369. 'product_ot_price' => $product['ot_price'] ?? 0,
  370. 'skus' => $skus
  371. ];
  372. // 比较价格,判断是否需要下架
  373. $priceChanged = $this->checkPriceChange($localPrices, $syncPrices);
  374. if ($priceChanged['result'] ?? false) {
  375. // 价格变动超过阈值,下架商品并记录
  376. // $this->offlineProductWithRecord($product, $syncData, $localPrices, $syncPrices);
  377. $changeSkuAttrDataList = [];
  378. foreach ($skus as $sku) {
  379. $edit_price = bcadd(
  380. $sku['price'],
  381. (string)$priceChanged['sku_difference_map'][$sku['gong_sku_id']] ?? ($priceChanged['spu_difference'] ?? '0.00'),
  382. 2
  383. );
  384. Log::info('商品价格变动:' . json_encode([
  385. 'product_id' => $product['product_id'],
  386. 'sku_id' => $sku['gong_sku_id'],
  387. 'before_price' => $sku['price'],
  388. 'edit_price' => $edit_price
  389. ])
  390. );
  391. $changeSkuAttrDataList[] = [
  392. 'sku_id' => $sku['gong_sku_id'],
  393. 'edit_price' => $edit_price
  394. ];
  395. }
  396. // 新规则 根据价格变动 增减商品售价
  397. /** @var ProductRepository $productRepository */
  398. $productRepository = app()->make(ProductRepository::class);
  399. $productRepository->editPrice($product['product_id'], $changeSkuAttrDataList);
  400. // 如果是系统下架的商品 则对商品重新上架
  401. if ($product['is_show'] == ProductEnum::IS_SHOW['No']['code'] && $product['is_status'] == ProductEnum::IS_STATUS['Automatic']['code']) {
  402. $this->restoreProduct($product['spu_id']);
  403. }
  404. } else {
  405. // 价格未变动或变动很小,只更新图片
  406. $this->updateProductImages($spuId);
  407. }
  408. }
  409. } catch (DataNotFoundException|ModelNotFoundException|DbException $e) {
  410. }
  411. }
  412. /**
  413. * 检查价格是否变动超过阈值
  414. */
  415. protected function checkPriceChange($localPrices, $syncPrices)
  416. {
  417. $res = [
  418. 'result' => false,
  419. 'spu_difference' => 0.00,
  420. 'sku_difference_map' => []
  421. ];
  422. // 如果成本价不变 则没有必要更新商品信息
  423. // 如果没有SKU,比较商品主价格
  424. if (empty($localPrices['skus'])) {
  425. $localPrice = $localPrices['product_cost'];
  426. $syncPrice = $syncPrices[0]['cost_price'] ?? 0;
  427. if ($localPrice > 0 && $syncPrice > 0) {
  428. $changeRate = abs(($syncPrice - $localPrice) / $localPrice);
  429. $res['result'] = $changeRate > $this->config['price_change_rate'];
  430. $res['spu_difference'] = bcsub((string)$syncPrice, $localPrice, 2);
  431. }
  432. return $res;
  433. }
  434. // 如果有SKU,匹配SKU进行比较
  435. foreach ($localPrices['skus'] as $localSku) {
  436. foreach ($syncPrices as $syncSku) {
  437. // 尝试匹配SKU
  438. if ($this->matchSku($localSku, $syncSku)) {
  439. $localPrice = $localSku['cost'] ?? 0;
  440. $syncPrice = $syncSku['cost_price'] ?? 0;
  441. if ($localPrice > 0 && $syncPrice > 0) {
  442. $changeRate = abs(($syncPrice - $localPrice) / $localPrice);
  443. $res['result'] = $changeRate > $this->config['price_change_rate'];
  444. $res['sku_difference_map'][$localSku['gong_sku_id']] = bcsub((string)$syncPrice, $localPrice, 2);
  445. }
  446. }
  447. }
  448. }
  449. return $res;
  450. }
  451. /**
  452. * 匹配SKU(根据业务逻辑实现)
  453. */
  454. protected function matchSku($localSku, $syncSku)
  455. {
  456. // 根据SKU ID匹配
  457. if (!empty($localSku['gong_sku_id']) && !empty($syncSku['sku_id'])) {
  458. return $localSku['gong_sku_id'] == $syncSku['sku_id'];
  459. }
  460. // 可以根据其他属性匹配,如规格等
  461. return false;
  462. }
  463. /**
  464. * 下架商品并记录变动
  465. */
  466. protected function offlineProductWithRecord($product, $syncData, $localPrices, $syncPrices)
  467. {
  468. $productId = $product['product_id'];
  469. $spuId = $product['spu_id'];
  470. // 1. 收集变动前数据
  471. $beforeData = [
  472. 'product_id' => $productId,
  473. 'spu_id' => $spuId,
  474. 'title' => $product['store_name'] ?? '',
  475. 'status' => $product['is_show'] ?? 0,
  476. 'price_info' => $localPrices,
  477. 'images' => [
  478. 'image' => $product['image'] ?? '',
  479. 'slider_image' => $product['slider_image'] ?? ''
  480. ],
  481. 'record_time' => date('Y-m-d H:i:s')
  482. ];
  483. // 2. 下架商品
  484. Db::name('store_product')
  485. ->where('product_id', $productId)
  486. ->update([
  487. 'is_show' => ProductEnum::IS_SHOW['No']['code'],
  488. 'status' => ProductEnum::STATUS['Withdrawn']['code'],
  489. 'is_status' => ProductEnum::IS_STATUS['Automatic']['code']
  490. ]);
  491. // 3. 收集变动后数据
  492. $afterData = [
  493. 'spu_id' => $spuId,
  494. 'sync_price_info' => $syncPrices,
  495. 'sync_title' => $syncData['title'],
  496. 'sync_status' => $syncData['status'],
  497. 'sync_time' => date('Y-m-d H:i:s')
  498. ];
  499. // 4. 记录变动日志
  500. $this->recordChangeLog(
  501. $productId,
  502. $spuId,
  503. 'price_change',
  504. '第三方平台价格变动超过阈值',
  505. $beforeData,
  506. $afterData
  507. );
  508. Log::info("商品下架: product_id={$productId}, spu_id={$spuId}, 原因: 价格变动");
  509. }
  510. /**
  511. * 更新商品图片(基于原有updata_product_img方法)
  512. */
  513. protected function updateProductImages($spuId)
  514. {
  515. // 1. 验证该SPU是否已上架
  516. $productId = Db::name('store_product')
  517. ->where('spu_id', $spuId)
  518. ->value('product_id');
  519. if (!$productId) {
  520. return true;
  521. }
  522. // 2. 查找微唯宝商品
  523. $data = Db::name('douhuomall')
  524. ->where('spu_id', $spuId)
  525. ->find();
  526. if (!$data) {
  527. return false;
  528. }
  529. // 3. 解析商品信息
  530. $spuIdInfo = json_decode($data['spuId_info'], true);
  531. // 4. 处理图片数据
  532. $sliderImage = $spuIdInfo['detail_img_list'] ?: '[]';
  533. $image = $spuIdInfo['cover_url'] ?? '';
  534. if (is_string($sliderImage)) {
  535. $sliderImage = implode(',', json_decode($sliderImage, true));
  536. } else if (is_array($sliderImage)) {
  537. $sliderImage = implode(',', $sliderImage);
  538. } else {
  539. $sliderImage = $image;
  540. }
  541. // 5. 更新产品图片数据
  542. $updateData = [
  543. 'image' => $image,
  544. 'slider_image' => $sliderImage,
  545. ];
  546. Db::name('store_product')
  547. ->where('product_id', $productId)
  548. ->update($updateData);
  549. // 6. 更新商品详情
  550. $detail = $spuIdInfo['detail'] ?? '';
  551. if (!is_null(json_decode($detail))) {
  552. $detailList = json_decode($detail);
  553. $newDetail = '';
  554. foreach ($detailList as $value) {
  555. $newDetail .= "<img src='{$value}' referrerpolicy='no-referrer' /></img>";
  556. }
  557. $detail = $newDetail;
  558. }
  559. Db::name('store_product_content')
  560. ->where('product_id', $productId)
  561. ->update(['content' => $detail]);
  562. return true;
  563. }
  564. /**
  565. * 处理需要下架的商品(第三方已下架)
  566. */
  567. protected function handleOfflineProducts($localSpuIds, $syncedSpuIds)
  568. {
  569. $offlineSpuIds = array_diff($localSpuIds, $syncedSpuIds);
  570. if (empty($offlineSpuIds)) {
  571. return;
  572. }
  573. foreach ($offlineSpuIds as $spuId) {
  574. $this->offlineProductBySpuId($spuId, 'third_party_offline');
  575. }
  576. Log::info("下架第三方已下架商品" . json_encode(['count' => count($offlineSpuIds), 'spu_ids' => $offlineSpuIds]));
  577. }
  578. /**
  579. * 根据SPU ID下架商品
  580. */
  581. protected function offlineProductBySpuId($spuId, $reason)
  582. {
  583. // 查找已上架的商品
  584. $products = Db::name('store_product')
  585. ->where('spu_id', $spuId)
  586. ->select();
  587. $douhuomallData = Db::name('douhuomall')->where('spu_id', $spuId)->select()->toArray();
  588. foreach ($products as $product) {
  589. // 下架商品
  590. Db::name('store_product')
  591. ->where('product_id', $product['product_id'])
  592. ->update([
  593. 'is_show' => ProductEnum::IS_SHOW['No']['code'],
  594. 'status' => ProductEnum::STATUS['Withdrawn']['code'],
  595. 'is_status' => ProductEnum::IS_STATUS['Automatic']['code']
  596. ]);
  597. // 记录日志
  598. $this->recordChangeLog(
  599. $product['product_id'],
  600. $spuId,
  601. 'third_party_offline',
  602. $reason,
  603. ['product' => $product, 'douhuomallData' => $douhuomallData],
  604. []
  605. );
  606. }
  607. // 从douhuomall表中删除
  608. Db::name('douhuomall')->where('spu_id', $spuId)->delete();
  609. }
  610. /**
  611. * 记录变动日志
  612. */
  613. protected function recordChangeLog($productId, $spuId, $changeType, $reason, $beforeData, $afterData)
  614. {
  615. $logData = [
  616. 'product_id' => $productId,
  617. 'spu_id' => $spuId,
  618. 'change_type' => $changeType,
  619. 'change_reason' => $reason,
  620. 'before_data' => json_encode($beforeData, JSON_UNESCAPED_UNICODE),
  621. 'after_data' => json_encode($afterData, JSON_UNESCAPED_UNICODE),
  622. 'change_time' => time(),
  623. 'create_time' => time()
  624. ];
  625. Db::name('product_change_log')->insert($logData);
  626. }
  627. /**
  628. * 数组排序(从原有add方法中提取)
  629. */
  630. protected function arrSort($array, $keys, $sort = SORT_DESC)
  631. {
  632. $keysValue = [];
  633. foreach ($array as $k => $v) {
  634. $keysValue[$k] = $v[$keys] ?? 0;
  635. }
  636. array_multisort($keysValue, $sort, $array);
  637. return $array;
  638. }
  639. /**
  640. * 计算成本价
  641. */
  642. protected function calculateCostPrice($platPrice)
  643. {
  644. return bcadd((string)$platPrice, bcmul((string)$platPrice, '0.03', 4), 2);
  645. }
  646. /**
  647. * 计算市场价
  648. */
  649. protected function calculateMarketPrice($platPrice)
  650. {
  651. $costPrice = $this->calculateCostPrice($platPrice);
  652. return bcadd($costPrice, bcmul($costPrice, '0.2', 4), 2);
  653. }
  654. /**
  655. * 计算利润率
  656. */
  657. protected function calculateProfit($platPrice)
  658. {
  659. $costPrice = $this->calculateCostPrice($platPrice);
  660. $marketPrice = $this->calculateMarketPrice($platPrice);
  661. return bcdiv($marketPrice, $costPrice, 2);
  662. }
  663. /**
  664. * 计算毛利
  665. */
  666. protected function calculateMerProfit($platPrice)
  667. {
  668. $costPrice = $this->calculateCostPrice($platPrice);
  669. $marketPrice = $this->calculateMarketPrice($platPrice);
  670. return bcsub($marketPrice, $costPrice, 2);
  671. }
  672. /**
  673. * 记录错误
  674. */
  675. protected function logError($message, \Exception $e)
  676. {
  677. $errorInfo = [
  678. 'message' => $message,
  679. 'file' => $e->getFile(),
  680. 'line' => $e->getLine(),
  681. 'error' => $e->getMessage()
  682. ];
  683. Log::error('商品同步错误' . json_encode($errorInfo, JSON_UNESCAPED_UNICODE));
  684. Db::name('log')->insert([
  685. 'data' => json_encode($errorInfo, JSON_UNESCAPED_UNICODE),
  686. 'time' => date('Y-m-d H:i:s')
  687. ]);
  688. }
  689. // 借用 app\controller\merchant\gong\Goods.php 控制器内方法
  690. /**
  691. * 将 微唯宝商品 选品(加入) 到系统内部商户商品表
  692. * @param $spu_id
  693. * @param $mer_id
  694. * @return bool
  695. */
  696. public function insert_product($spu_id, $mer_id)
  697. {
  698. try {
  699. // 1、获取商品信息
  700. $data = Db::name('douhuomall')->where('spu_id', $spu_id)->find();
  701. // 2、是否为多规格
  702. $sku_data = json_decode($data['skuId_info'], true);
  703. $spec_type = count($sku_data);
  704. if ($spec_type > 1) {
  705. $spec_type = 1;
  706. } else {
  707. $spec_type = 0;
  708. }
  709. // 3、商品主图
  710. $spuId_info = json_decode($data['spuId_info'], true);
  711. $slider_image = $spuId_info['detail_img_list'] ?? '';
  712. $image = $spuId_info['cover_url'] ?? '';
  713. if (empty($slider_image)) {
  714. $slider_image = $image;
  715. } else if (is_string($slider_image)) {
  716. $slider_image = implode(',', json_decode($slider_image, true));
  717. } else if (is_array($slider_image)) {
  718. $slider_image = implode(',', $slider_image);
  719. }
  720. $yanglaojin_scale = (!empty($data['pension']) && !empty($data['market_price'])) ? bcdiv($data['pension'], $data['market_price'], 2) : 0;
  721. // 4、商品入库数据
  722. $product_insert_data = [
  723. 'mer_id' => $mer_id,
  724. 'image' => $image,
  725. 'slider_image' => $slider_image,
  726. 'store_name' => $data['title'],
  727. 'store_info' => 1,
  728. 'keyword' => mb_substr($data['title'], 0, 3),
  729. 'is_show' => 1,
  730. 'status' => 1,
  731. 'cate_id' => $data['cate_ids'],
  732. 'unit_name' => '个',
  733. 'price' => $data['market_price'],
  734. 'cost' => $data['cost_price'],
  735. 'ot_price' => $data['ot_price'],
  736. 'stock' => '1000',
  737. 'spec_type' => $spec_type,
  738. 'extension_type' => 1,
  739. 'mer_status' => 1,
  740. 'is_used' => 1,
  741. 'old_product_id' => $data['id'],
  742. 'volunteer' => 0,
  743. 'type' => 1,
  744. 'pension' => '0.00',
  745. 'commission' => 5,
  746. 'spu_id' => $data['spu_id'],
  747. 'temp_id' => 105,
  748. 'plate_mer_profit' => $data['mer_profit'],
  749. 'concession_pri' => $data['mer_profit'],
  750. 'yanglaojin_scale' => $yanglaojin_scale
  751. ];
  752. $insert_id = Db::name('store_product')->insertGetId($product_insert_data);
  753. // 5、更新商品详情表
  754. $detail = $spuId_info['detail'];
  755. if (!is_null(json_decode($detail))) {
  756. $detail_list = json_decode($detail);
  757. $detail = '';
  758. foreach ($detail_list as $value) {
  759. $detail .= '<img src="' . $value . '"></img>';
  760. }
  761. }
  762. Db::name('store_product_content')->insert(['content' => $detail, 'product_id' => $insert_id]);
  763. // 6、
  764. $this->insert_sku($insert_id, $sku_data);
  765. return true;
  766. } catch (\Exception $e) {
  767. Log::error($spu_id . "加入选品失败:" . $e->getMessage());
  768. return false;
  769. }
  770. }
  771. // 借用 app\controller\merchant\gong\Goods.php 控制器内方法
  772. public function insert_sku($id, $data)
  773. {
  774. // 1、获取 所有SKU的 规格属性值
  775. $sku_attr_data = array_column($data, 'attribute_json');
  776. $name = '';
  777. // 2、写入 商品属性表
  778. /** @var ProductRepository $ProductRepository */
  779. $ProductRepository = app()->make(ProductRepository::class);
  780. if (empty($sku_attr_data[0][0])) {
  781. // 写入商品属性表
  782. Db::name('store_product_attr')->insert(['product_id' => $id, 'attr_name' => '规格', 'attr_values' => $name]);
  783. $key = '';
  784. $num = 1;
  785. // 写入 SKU 商品属性值表
  786. foreach ($data as $k => $v) {
  787. Db::name('store_product_attr_value')->insert([
  788. 'product_id' => $id,
  789. 'detail' => json_encode(['规格' => $key]),
  790. 'sku' => $key,
  791. 'image' => $v['img_url'],
  792. 'cost' => $v['cost_price'],
  793. 'ot_price' => $v['retail_price'],
  794. 'price' => $v['market_price'],
  795. 'unique' => $ProductRepository->setUnique($id, $v['sku_id'], 0),
  796. 'stock' => 100,
  797. 'cost_price' => $v['cost_price'],
  798. 'extension_one' => 5,
  799. 'gong_sku_id' => $v['sku_id'],
  800. 'gong_mer_profit' => $v['mer_profit'],
  801. 'gong_pension' => $v['yanglaojin'],
  802. 'gong_market_price' => $v['market_price'],
  803. 'plate_mer_profit' => $v['mer_profit']
  804. ]);
  805. $key = '';
  806. $num++;
  807. }
  808. } else {
  809. foreach ($sku_attr_data as $kk => $vv) {
  810. foreach ($vv as $k1 => $v1) {
  811. $name .= $v1['val'];
  812. }
  813. $name .= '-!-';
  814. }
  815. $name = rtrim($name, '-!-');
  816. Db::name('store_product_attr')->insert(['product_id' => $id, 'attr_name' => '规格', 'attr_values' => $name]);
  817. $key = '';
  818. $num = 1;
  819. foreach ($data as $k => $v) {
  820. foreach ($v['attribute_json'] as $k2 => $v2) {
  821. $key .= $v2['val'];
  822. }
  823. Db::name('store_product_attr_value')->insert([
  824. 'product_id' => $id,
  825. 'detail' => json_encode(['规格' => $key]),
  826. 'sku' => $key,
  827. 'image' => $v['img_url'],
  828. 'cost' => $v['cost_price'],
  829. 'ot_price' => $v['retail_price'],
  830. 'price' => $v['market_price'],
  831. 'unique' => $ProductRepository->setUnique((int)$id, $v['sku_id'], 0),
  832. 'stock' => 100,
  833. 'cost_price' => $v['cost_price'],
  834. 'extension_one' => 5,
  835. 'gong_sku_id' => $v['sku_id'],
  836. 'gong_mer_profit' => $v['mer_profit'],
  837. 'gong_pension' => $v['yanglaojin'],
  838. 'gong_market_price' => $v['market_price'],
  839. 'plate_mer_profit' => $v['mer_profit']
  840. ]);
  841. $key = '';
  842. $num++;
  843. }
  844. }
  845. }
  846. }