100, // 每页拉取数量 'max_pages' => 0, // 最大页数限制 'price_change_rate' => 0.0, // 价格变动比率阈值(10%) 'mer_id' => CommonEnum::DESIGN_MERCHANT_ID['WonderfulLiving']['code'] ]; protected function configure() { // 指令配置 $this->setName('wiwibaoproductsync') ->setDescription('the wiwibaoproductsync command'); } protected function execute(Input $input, Output $output) { $output->writeln('定时任务执行中...'); $this->syncGoodsFromGong(); $output->writeln('定时任务执行完成'); } /** * 同步微唯宝商品数据(主方法) */ public function syncGoodsFromGong() { try { set_time_limit(0); // 1. 批量拉取所有商品数据 $allProducts = $this->fetchAllProducts(); if (empty($allProducts)) { Log::info('未拉取到任何商品数据'); return 'success'; } // 2. 获取本地所有SPU ID $douhuomallDataMap = []; $douhuomallDataList = Db::name('douhuomall') ->select() ->toArray(); if (!empty($douhuomallDataList)) { $douhuomallDataMap = array_column($douhuomallDataList, null, 'spu_id'); } $syncedSpuIds = []; // 3. 批量处理商品数据 foreach ($allProducts as $product) { try { $spuId = $this->processSingleProduct($product, $douhuomallDataMap[$product['goods_id']] ?? []); if ($spuId) { $syncedSpuIds[] = $spuId; } } catch (\Exception $e) { $this->logError("处理商品 {$product['goods_id']} 失败: ", $e); continue; } } // 4. 处理需要下架的商品(本地有但第三方已下架) $this->handleOfflineProducts($syncedSpuIds, $douhuomallDataMap); return 'success'; } catch (\Exception $e) { $this->logError('商品同步失败', $e); return 'error'; } } /** * 批量拉取所有商品数据 */ protected function fetchAllProducts() { $allData = []; // 第一页 $firstPageData = GongApiRequest::get_goods_list(1, $this->config['page_size']); if (empty($firstPageData['list'])) { return []; } $allData = $firstPageData['list']; $totalPages = $firstPageData['pages'] ?? 1; if (!empty($this->config['max_pages'])) { $totalPages = min($totalPages, $this->config['max_pages']); } // 并行获取剩余页面 for ($page = 2; $page <= $totalPages; $page++) { $pageData = GongApiRequest::get_goods_list($page, $this->config['page_size']); if (!empty($pageData['list'])) { $allData = array_merge($allData, $pageData['list']); } } return $allData; } /** * 处理单个商品 */ protected function processSingleProduct($productData, $douhuomallData) { $goodsId = $productData['goods_id']; // 1. 获取商品详情 $goodsInfo = GongApiRequest::get_goods_info($goodsId); if (!$goodsInfo) { return null; } // 2. 处理SKU信息 if (empty($goodsInfo['sku_list'])) { return null; } $skuInfo = []; foreach ($goodsInfo['sku_list'] as $sku) { if (empty($sku['plat_price'])) { continue; } $processedSku = [ 'sku_id' => $sku['sku_id'] ?? 0, 'plat_price' => $sku['plat_price'], 'img_url' => $sku['main_img'], 'retail_price' => $sku['retail_price'] ?? 0, 'cost_price' => $this->calculateCostPrice($sku['plat_price']), 'market_price' => $this->calculateMarketPrice($sku['plat_price']), 'profit' => $this->calculateProfit($sku['plat_price']), 'mer_profit' => $this->calculateMerProfit($sku['plat_price']), 'yanglaojin' => '0.00', 'main_img' => $sku['main_img'] ?? '', 'attribute_json' => !empty($sku['attr']) ? $sku['attr'] : [['val' => '默认', 'name' => '默认']] ]; $skuInfo[] = array_merge($sku, $processedSku); } // 4. 构建保存数据 $imageData = [ 'cover_url' => $goodsInfo['main_img'] ?? '', 'detail_images' => $goodsInfo['detail_img'] ?? [], 'detail_img_list' => $goodsInfo['detail_img'] ?? [] // 为了兼容以前写的代码,建议保留这个数据项,之前用的是detail_img_list字段,但我感觉他们意义一样 ]; // 构建详情HTML $detailHtml = $this->buildDetailHtml($imageData['detail_images']); $saveData = [ 'spu_id' => $goodsInfo['goods_id'], 'skuId_info' => $skuInfo, 'sys_id' => '112', 'goods_id' => $goodsInfo['goods_id'], 'spuId_info' => array_merge( $goodsInfo, [ 'cover_url' => $imageData['cover_url'], 'detail' => $detailHtml, 'detail_img_list' => $imageData['detail_img_list'] ] ), 'sku_id' => $skuInfo[0]['sku_id'] ?? 0, 'status' => $goodsInfo['status'] ?? 0, 'cate_ids' => $goodsInfo['cate_ids'] ?? '', 'title' => $goodsInfo['spu_name'] ?? '', 'cxb_cate_id' => 0, 'third_update_time' => $goodsInfo['update_time'] ?? '' ]; // 5. 保存到本地(使用原有的add方法逻辑) try { $spuId = $this->addToDouhuomall($saveData, $douhuomallData); } catch (DbException $e) { return 0; } return $spuId; } /** * 构建详情HTML */ protected function buildDetailHtml($detailImages): string { if (empty($detailImages)) { return ''; } $detailHtml = ''; foreach ((array)$detailImages as $imgUrl) { $detailHtml .= ""; } return $detailHtml; } /** * @param $data * @param $findData * @return mixed * @throws DbException */ protected function addToDouhuomall($data, $findData) { try { // 对SKU按成本价排序,获取最低成本价的SKU $skuInfo = $this->arrSort($data['skuId_info'], 'cost_price', SORT_ASC); // 计算价格字段 $data['cost_price'] = $skuInfo[0]['cost_price'] ?? 0; $data['market_price'] = $skuInfo[0]['market_price'] ?? 0; $data['ot_price'] = $skuInfo[0]['retail_price'] ?? 0; $data['mer_profit'] = $skuInfo[0]['mer_profit'] ?? 0; $data['pension'] = $skuInfo[0]['yanglaojin'] ?? 0; $data['profit'] = (empty($data['cost_price']) || empty($data['market_price'])) ? 0 : bcdiv((string)$data['market_price'], (string)$data['cost_price'], 2); $now = date('Y-m-d H:i:s'); if (empty($findData)) { // 新增 $insertData = [ 'spu_id' => $data['spu_id'], 'skuId_info' => json_encode($data['skuId_info'], JSON_UNESCAPED_UNICODE), 'sys_id' => $data['sys_id'], 'goods_id' => $data['goods_id'], 'spuId_info' => json_encode($data['spuId_info'], JSON_UNESCAPED_UNICODE), 'create_time' => $now, 'update_time' => $now, 'sku_id' => $data['sku_id'], 'status' => $data['status'], 'cate_ids' => $data['cate_ids'], 'title' => $data['title'], 'cost_price' => $data['cost_price'], 'market_price' => $data['market_price'], 'ot_price' => $data['ot_price'], 'profit' => $data['profit'], 'mer_profit' => $data['mer_profit'], 'pension' => $data['pension'], 'cxb_cate_id' => $data['cxb_cate_id'], 'third_update_time' => $data['third_update_time'] ]; Db::name('douhuomall')->insert($insertData); // 创建完商品后立马上架 $this->insert_product($data['spu_id'], $this->config['mer_id']); } else { // 更新 if (empty($findData['third_update_time']) || strtotime($data['third_update_time']) > strtotime($findData['third_update_time'])) { $updateData = [ 'status' => $data['status'], 'update_time' => $now, 'cost_price' => $data['cost_price'], 'market_price' => $data['market_price'], 'ot_price' => $data['ot_price'], 'mer_profit' => $data['mer_profit'], 'pension' => $data['pension'], 'profit' => $data['profit'], 'skuId_info' => json_encode($data['skuId_info'], JSON_UNESCAPED_UNICODE), 'spuId_info' => json_encode($data['spuId_info'], JSON_UNESCAPED_UNICODE), 'title' => $data['title'], 'cate_ids' => $data['cate_ids'], 'cxb_cate_id' => $data['cxb_cate_id'], 'third_update_time' => $data['third_update_time'] ]; Db::name('douhuomall') ->where('spu_id', $findData['spu_id']) ->update($updateData); $productList = Db::name('store_product') ->where('spu_id', $findData['spu_id']) // ->where('is_show', ProductEnum::IS_SHOW['Yes']['code']) ->where('is_del', ProductEnum::IS_DEL['No']['code']) // ->where('status', '<>', ProductEnum::STATUS['Withdrawn']['code']) ->select() ->toArray(); $productIdList = array_column($productList, 'product_id'); // 将绑定的商品进行下架 Db::name('store_product') ->whereIn('product_id', $productIdList) ->update([ // 'is_show' => ProductEnum::IS_SHOW['No']['code'], 'is_del' => ProductEnum::IS_DEL['Yes']['code'], // 'status' => ProductEnum::STATUS['Withdrawn']['code'], // 'is_status' => ProductEnum::IS_STATUS['Automatic']['code'] ]); /** @var DouhuomallChangeLogEntity $douhuomallChangeLogEntity */ $douhuomallChangeLogEntity = DouhuomallChangeLogEntity::newInstance(); $douhuomallChangeLogEntity->setSpuId($findData['spu_id']) ->setDataJson($findData) ->setEffectProductJson($productIdList) ->setCreateTime(date('Y-m-d H:i:s')); /** @var DouhuomallChangeLogRepository $douhuomallChangeLogRepository */ $douhuomallChangeLogRepository = app()->make(DouhuomallChangeLogRepository::class); $douhuomallChangeLogRepository->createByEntity($douhuomallChangeLogEntity); // 将商品重新上架 // 创建完商品后立马上架 $this->insert_product($data['spu_id'], $this->config['mer_id']); } } return $data['spu_id']; } catch (\Exception $e) { $this->logError('保存商品到douhuomall失败: ', $e); throw $e; } } /** * 恢复商品上架状态 */ protected function restoreProduct($spuId) { Db::name('store_product') ->where('spu_id', $spuId) ->update([ 'is_show' => ProductEnum::IS_SHOW['Yes']['code'], 'status' => ProductEnum::STATUS['Approved']['code'] ]); Log::info("恢复商品上架(恢复到仓库中)状态: spu_id={$spuId}"); } /** * 处理需要下架的商品(第三方已下架) */ protected function handleOfflineProducts($syncedSpuIds, $douhuomallDataMap) { $localSpuIds = array_keys($douhuomallDataMap); $offlineSpuIds = array_diff($localSpuIds, $syncedSpuIds); if (empty($offlineSpuIds)) { return; } $productList = Db::name('store_product') ->whereIn('spu_id', $offlineSpuIds) // ->where('is_show', ProductEnum::IS_SHOW['Yes']['code']) ->where('is_del', ProductEnum::IS_DEL['No']['code']) // ->where('status', '<>', ProductEnum::STATUS['Withdrawn']['code']) ->field('product_id, spu_id, is_show, is_del, status') ->select() ->toArray(); if (empty($productList)) { return; } $productIdList = []; $spuIdMap = []; foreach ($productList as $item) { $productIdList[] = $item['product_id']; $spuIdMap[$item['spu_id']][] = $item['product_id']; } // 将绑定的商品进行下架 Db::name('store_product') ->whereIn('product_id', $productIdList) ->update([ // 'is_show' => ProductEnum::IS_SHOW['No']['code'], 'is_del' => ProductEnum::IS_DEL['Yes']['code'], // 'status' => ProductEnum::STATUS['Withdrawn']['code'], // 'is_status' => ProductEnum::IS_STATUS['Automatic']['code'] ]); foreach ($spuIdMap as $spuId => $productIdListGroupBySpuId) { /** @var DouhuomallChangeLogEntity $douhuomallChangeLogEntity */ $douhuomallChangeLogEntity = DouhuomallChangeLogEntity::newInstance(); $douhuomallChangeLogEntity->setSpuId($spuId) ->setDataJson($douhuomallDataMap[$spuId]) ->setEffectProductJson($productIdListGroupBySpuId) ->setCreateTime(date('Y-m-d H:i:s')); /** @var DouhuomallChangeLogRepository $douhuomallChangeLogRepository */ $douhuomallChangeLogRepository = app()->make(DouhuomallChangeLogRepository::class); $douhuomallChangeLogRepository->createByEntity($douhuomallChangeLogEntity); } // foreach ($offlineSpuIds as $spuId) { // $this->offlineProductBySpuId($spuId, 'third_party_offline'); // } Log::info("下架第三方已下架商品" . json_encode(['count' => count($offlineSpuIds), 'spu_ids' => $offlineSpuIds])); } /** * 数组排序(从原有add方法中提取) */ protected function arrSort($array, $keys, $sort = SORT_DESC) { $keysValue = []; foreach ($array as $k => $v) { $keysValue[$k] = $v[$keys] ?? 0; } array_multisort($keysValue, $sort, $array); return $array; } /** * 计算成本价 */ protected function calculateCostPrice($platPrice) { return bcadd((string)$platPrice, bcmul((string)$platPrice, '0.73', 4), 2); } /** * 计算市场价 */ protected function calculateMarketPrice($platPrice) { $costPrice = $this->calculateCostPrice($platPrice); return bcadd($costPrice, bcmul($costPrice, '0.2', 4), 2); } /** * 计算利润率 */ protected function calculateProfit($platPrice) { $costPrice = $this->calculateCostPrice($platPrice); $marketPrice = $this->calculateMarketPrice($platPrice); return bcdiv($marketPrice, $costPrice, 2); } /** * 计算毛利 */ protected function calculateMerProfit($platPrice) { $costPrice = $this->calculateCostPrice($platPrice); $marketPrice = $this->calculateMarketPrice($platPrice); return bcsub($marketPrice, $costPrice, 2); } /** * 记录错误 */ protected function logError($message, \Exception $e) { $errorInfo = [ 'message' => $message, 'error_message' => $e->getMessage(), 'error_file' => $e->getFile(), 'error_line' => $e->getLine(), 'error_code' => $e->getCode(), 'exception_class' => get_class($e), 'trace' => $e->getTraceAsString() ]; Log::error($message); Log::error($message . json_encode($errorInfo, JSON_UNESCAPED_UNICODE)); Db::name('log')->insert([ 'data' => json_encode($errorInfo, JSON_UNESCAPED_UNICODE), 'time' => date('Y-m-d H:i:s') ]); } // 借用 app\controller\merchant\gong\Goods.php 控制器内方法 /** * 将 微唯宝商品 选品(加入) 到系统内部商户商品表 * @param $spu_id * @param $mer_id * @return bool */ public function insert_product($spu_id, $mer_id) { try { // 1、获取商品信息 $data = Db::name('douhuomall')->where('spu_id', $spu_id)->find(); // 2、是否为多规格 $sku_data = json_decode($data['skuId_info'], true); $spec_type = count($sku_data); if ($spec_type > 1) { $spec_type = 1; } else { $spec_type = 0; } // 3、商品主图 $spuId_info = json_decode($data['spuId_info'], true); $slider_image = $spuId_info['detail_img_list'] ?? ''; $image = $spuId_info['cover_url'] ?? ''; if (empty($slider_image)) { $slider_image = $image; } else if (is_string($slider_image)) { $slider_image = implode(',', json_decode($slider_image, true)); } else if (is_array($slider_image)) { $slider_image = implode(',', $slider_image); } $yanglaojin_scale = (!empty($data['pension']) && !empty($data['market_price'])) ? bcdiv($data['pension'], $data['market_price'], 2) : 0; // 4、商品入库数据 $product_insert_data = [ 'mer_id' => $mer_id, 'image' => $image, 'slider_image' => $slider_image, 'store_name' => $data['title'], 'store_info' => 1, 'keyword' => mb_substr($data['title'], 0, 3), 'is_show' => 1, 'status' => 1, 'cate_id' => $data['cate_ids'], 'unit_name' => '个', 'price' => $data['market_price'], 'cost' => $data['cost_price'], 'ot_price' => $data['ot_price'], 'stock' => '1000', 'spec_type' => $spec_type, 'extension_type' => 1, 'mer_status' => 1, 'is_used' => 1, 'old_product_id' => $data['id'], 'volunteer' => 0, 'type' => 1, 'pension' => '0.00', 'commission' => 5, 'spu_id' => $data['spu_id'], 'temp_id' => 105, 'plate_mer_profit' => $data['mer_profit'], 'concession_pri' => $data['mer_profit'], 'yanglaojin_scale' => $yanglaojin_scale ]; $insert_id = Db::name('store_product')->insertGetId($product_insert_data); // 5、更新商品详情表 $detail = $spuId_info['detail']; if (!is_null(json_decode($detail))) { $detail_list = json_decode($detail); $detail = ''; foreach ($detail_list as $value) { $detail .= ''; } } Db::name('store_product_content')->insert(['content' => $detail, 'product_id' => $insert_id]); // 6、 $this->insert_sku($insert_id, $sku_data); return true; } catch (\Exception $e) { $this->logError($spu_id . "加入选品失败:", $e); return false; } } // 借用 app\controller\merchant\gong\Goods.php 控制器内方法 public function insert_sku($id, $data) { // 1、获取 所有SKU的 规格属性值 $sku_attr_data = array_column($data, 'attribute_json'); $name = ''; // 2、写入 商品属性表 /** @var ProductRepository $ProductRepository */ $ProductRepository = app()->make(ProductRepository::class); if (empty($sku_attr_data[0][0])) { // 写入商品属性表 Db::name('store_product_attr')->insert(['product_id' => $id, 'attr_name' => '规格', 'attr_values' => $name]); $key = ''; $num = 1; // 写入 SKU 商品属性值表 foreach ($data as $k => $v) { Db::name('store_product_attr_value')->insert([ 'product_id' => $id, 'detail' => json_encode(['规格' => $key]), 'sku' => $key, 'image' => $v['img_url'], 'cost' => $v['cost_price'], 'ot_price' => $v['retail_price'], 'price' => $v['market_price'], 'unique' => $ProductRepository->setUnique($id, $v['sku_id'], 0), 'stock' => 100, 'cost_price' => $v['cost_price'], 'extension_one' => 5, 'gong_sku_id' => $v['sku_id'], 'gong_mer_profit' => $v['mer_profit'], 'gong_pension' => $v['yanglaojin'], 'gong_market_price' => $v['market_price'], 'plate_mer_profit' => $v['mer_profit'] ]); $key = ''; $num++; } } else { foreach ($sku_attr_data as $kk => $vv) { foreach ($vv as $k1 => $v1) { $name .= $v1['val']; } $name .= '-!-'; } $name = rtrim($name, '-!-'); Db::name('store_product_attr')->insert(['product_id' => $id, 'attr_name' => '规格', 'attr_values' => $name]); $key = ''; $num = 1; foreach ($data as $k => $v) { foreach ($v['attribute_json'] as $k2 => $v2) { $key .= $v2['val']; } Db::name('store_product_attr_value')->insert([ 'product_id' => $id, 'detail' => json_encode(['规格' => $key]), 'sku' => $key, 'image' => $v['img_url'], 'cost' => $v['cost_price'], 'ot_price' => $v['retail_price'], 'price' => $v['market_price'], 'unique' => $ProductRepository->setUnique((int)$id, $v['sku_id'], 0), 'stock' => 100, 'cost_price' => $v['cost_price'], 'extension_one' => 5, 'gong_sku_id' => $v['sku_id'], 'gong_mer_profit' => $v['mer_profit'], 'gong_pension' => $v['yanglaojin'], 'gong_market_price' => $v['market_price'], 'plate_mer_profit' => $v['mer_profit'] ]); $key = ''; $num++; } } } }