| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 |
- <?php
- declare (strict_types=1);
- namespace app\command;
- use app\common\enum\douhuomall\DouhuomallEnum;
- use app\traits\GongApiRequest;
- use think\console\Command;
- use think\console\Input;
- use think\console\Output;
- use think\facade\Db;
- use think\facade\Log;
- class WiwibaoProductSync extends Command
- {
- use GongApiRequest;
- // 配置参数
- protected $config = [
- 'page_size' => 100, // 每页拉取数量
- 'max_pages' => 0, // 最大页数限制
- 'price_change_rate' => 0.1, // 价格变动比率阈值(10%)
- ];
- 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
- $localSpuIds = Db::name('douhuomall')->column('spu_id');
- $syncedSpuIds = [];
- // 3. 批量处理商品数据
- foreach ($allProducts as $product) {
- try {
- $spuId = $this->processSingleProduct($product);
- if ($spuId) {
- $syncedSpuIds[] = $spuId;
- }
- } catch (\Exception $e) {
- Log::error("处理商品 {$product['goods_id']} 失败: " . $e->getMessage());
- continue;
- }
- }
- // 4. 处理需要下架的商品(本地有但第三方已下架)
- $this->handleOfflineProducts($localSpuIds, $syncedSpuIds);
- 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)
- {
- $goodsId = $productData['goods_id'];
- // 1. 获取商品详情
- $goodsInfo = GongApiRequest::get_goods_info($goodsId);
- if (!$goodsInfo) {
- return null;
- }
- // 2. 处理图片信息
- $imageData = $this->processProductImages($goodsId, $goodsInfo);
- // 3. 处理SKU信息
- $skuInfo = $this->processSkuInfo($goodsInfo['sku_list'] ?? [], $goodsId);
- // 4. 构建保存数据
- $saveData = $this->buildSaveData($goodsInfo, $skuInfo, $imageData);
- // 5. 保存到本地(使用原有的add方法逻辑)
- $spuId = $this->addToDouhuomall($saveData);
- // 6. 检查并处理已上架商品的价格变动
- $this->checkAndProcessOnlineProduct($spuId, $saveData);
- return $spuId;
- }
- /**
- * 处理商品图片信息
- */
- protected function processProductImages($goodsId, $goodsInfo)
- {
- // 如果你需要保存图片到本地,可以在这里实现
- // 否则直接返回URL
- return [
- 'cover_url' => $goodsInfo['main_img'] ?? '',
- 'detail_images' => $goodsInfo['detail_img'] ?? [],
- 'detail_img_list' => $goodsInfo['detail_img_list'] ?? []
- ];
- }
- /**
- * 处理SKU信息
- */
- protected function processSkuInfo($skuList, $goodsId)
- {
- if (empty($skuList)) {
- return [];
- }
- $processedSkus = [];
- foreach ($skuList as $sku) {
- if (empty($sku['plat_price'])) {
- continue;
- }
- $processedSku = [
- 'sku_id' => $sku['sku_id'] ?? 0,
- 'plat_price' => $sku['plat_price'],
- 'img_url' => $sku['img_url'],
- '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'] ?? '',
- 'attr' => !empty($sku['attr']) ? $sku['attr'] : []
- ];
- $processedSkus[] = array_merge($sku, $processedSku);
- }
- return $processedSkus;
- }
- /**
- * 构建保存数据
- */
- protected function buildSaveData($goodsInfo, $skuInfo, $imageData)
- {
- // 构建详情HTML
- $detailHtml = $this->buildDetailHtml($imageData['detail_images']);
- return [
- '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
- ];
- }
- /**
- * 构建详情HTML
- */
- protected function buildDetailHtml($detailImages)
- {
- if (empty($detailImages)) {
- return '';
- }
- $detailHtml = '';
- foreach ((array)$detailImages as $imgUrl) {
- $detailHtml .= "<img src='{$imgUrl}' referrerpolicy='no-referrer' />";
- }
- return $detailHtml;
- }
- /**
- * 保存到douhuomall表(基于原有add方法)
- */
- protected function addToDouhuomall($data)
- {
- try {
- $findData = Db::name('douhuomall')->where('spu_id', $data['spu_id'])->find();
- // 对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']
- ];
- Db::name('douhuomall')->insert($insertData);
- } else {
- // 更新
- // 检查是否需要恢复已删除商品的上架状态
- $shouldRestore = $this->shouldRestoreProduct($findData, $data);
- if ($shouldRestore) {
- $this->restoreProduct($data['spu_id']);
- }
- $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']
- ];
- Db::name('douhuomall')
- ->where('spu_id', $findData['spu_id'])
- ->update($updateData);
- }
- return $data['spu_id'];
- } catch (\Exception $e) {
- Log::error("保存商品到douhuomall失败: " . $e->getMessage());
- throw $e;
- }
- }
- /**
- * 检查是否需要恢复已删除商品的上架状态
- */
- protected function shouldRestoreProduct($findData, $newData)
- {
- // 原有逻辑:如果货物属于已被删除状态,并且新更新进来的状态非删除状态
- // 并且成本价、零售价、市场价、利润率没有变动的话
- if ($findData['status'] == DouhuomallEnum::STATUS['Delete']['code']
- && $newData['status'] !== DouhuomallEnum::STATUS['Delete']['code']
- && $newData['cost_price'] == $findData['cost_price']
- && $newData['ot_price'] == $findData['ot_price']
- && $newData['market_price'] == $findData['market_price']
- && $newData['profit'] == $findData['profit']) {
- // 检查是否有已下架的商品
- $count = Db::name('store_product')
- ->where('spu_id', $newData['spu_id'])
- ->where('is_show', 0)
- ->where('is_status', 0)
- ->count();
- return $count > 0;
- }
- return false;
- }
- /**
- * 恢复商品上架状态
- */
- protected function restoreProduct($spuId)
- {
- Db::name('store_product')
- ->where('spu_id', $spuId)
- ->update(['is_show' => 1]);
- Log::info("恢复商品上架状态: spu_id={$spuId}");
- }
- /**
- * 检查并处理已上架商品
- */
- protected function checkAndProcessOnlineProduct($spuId, $syncData)
- {
- // 检查商品是否已上架
- $product = Db::name('store_product')
- ->where('spu_id', $spuId)
- ->find();
- if (!$product) {
- return;
- }
- // 获取本地商品价格信息
- $localPrices = $this->getLocalProductPrices($product['product_id']);
- // 解析同步数据中的价格
- $syncSkuInfo = $syncData['skuId_info'];
- $syncPrices = $this->extractPricesFromSkuInfo($syncSkuInfo);
- // 比较价格,判断是否需要下架
- $priceChanged = $this->checkPriceChange($localPrices, $syncPrices);
- if ($priceChanged) {
- // 价格变动超过阈值,下架商品并记录
- $this->offlineProductWithRecord($product, $syncData, $localPrices, $syncPrices);
- } else {
- // 价格未变动或变动很小,只更新图片
- $this->updateProductImages($spuId);
- }
- }
- /**
- * 获取本地商品价格信息
- */
- protected function getLocalProductPrices($productId)
- {
- $product = Db::name('store_product')
- ->where('product_id', $productId)
- ->field('price, cost, ot_price')
- ->find();
- // 获取SKU价格
- $skus = Db::name('store_product_attr_value')
- ->where('product_id', $productId)
- ->field('sku, price, cost')
- ->select()
- ->toArray();
- return [
- 'product_price' => $product['price'] ?? 0,
- 'product_cost' => $product['cost'] ?? 0,
- 'product_ot_price' => $product['ot_price'] ?? 0,
- 'skus' => $skus
- ];
- }
- /**
- * 从SKU信息中提取价格
- */
- protected function extractPricesFromSkuInfo($skuInfo)
- {
- $prices = [];
- foreach ($skuInfo as $sku) {
- $prices[] = [
- 'sku_id' => $sku['sku_id'] ?? '',
- 'market_price' => $sku['market_price'] ?? 0,
- 'cost_price' => $sku['cost_price'] ?? 0,
- 'retail_price' => $sku['retail_price'] ?? 0
- ];
- }
- return $prices;
- }
- /**
- * 检查价格是否变动超过阈值
- */
- protected function checkPriceChange($localPrices, $syncPrices)
- {
- // 如果没有SKU,比较商品主价格
- if (empty($localPrices['skus'])) {
- $localPrice = $localPrices['product_price'];
- $syncPrice = $syncPrices[0]['market_price'] ?? 0;
- if ($localPrice > 0 && $syncPrice > 0) {
- $changeRate = abs(($syncPrice - $localPrice) / $localPrice);
- return $changeRate > $this->config['price_change_rate'];
- }
- return false;
- }
- // 如果有SKU,匹配SKU进行比较
- foreach ($localPrices['skus'] as $localSku) {
- foreach ($syncPrices as $syncSku) {
- // 尝试匹配SKU
- if ($this->matchSku($localSku, $syncSku)) {
- $localPrice = $localSku['price'] ?? 0;
- $syncPrice = $syncSku['market_price'] ?? 0;
- if ($localPrice > 0 && $syncPrice > 0) {
- $changeRate = abs(($syncPrice - $localPrice) / $localPrice);
- if ($changeRate > $this->config['price_change_rate']) {
- return true;
- }
- }
- }
- }
- }
- return false;
- }
- /**
- * 匹配SKU(根据业务逻辑实现)
- */
- protected function matchSku($localSku, $syncSku)
- {
- // 根据SKU ID匹配
- if (!empty($localSku['sku']) && !empty($syncSku['sku_id'])) {
- return $localSku['sku'] == $syncSku['sku_id'];
- }
- // 可以根据其他属性匹配,如规格等
- return false;
- }
- /**
- * 下架商品并记录变动
- */
- protected function offlineProductWithRecord($product, $syncData, $localPrices, $syncPrices)
- {
- $productId = $product['product_id'];
- $spuId = $product['spu_id'];
- // 1. 收集变动前数据
- $beforeData = [
- 'product_id' => $productId,
- 'spu_id' => $spuId,
- 'title' => $product['store_name'] ?? '',
- 'status' => $product['is_show'] ?? 0,
- 'price_info' => $localPrices,
- 'images' => [
- 'image' => $product['image'] ?? '',
- 'slider_image' => $product['slider_image'] ?? ''
- ],
- 'record_time' => date('Y-m-d H:i:s')
- ];
- // 2. 下架商品
- Db::name('store_product')
- ->where('product_id', $productId)
- ->update([
- 'is_show' => 0,
- 'offline_time' => time(),
- 'offline_reason' => '第三方平台价格变动'
- ]);
- // 3. 收集变动后数据
- $afterData = [
- 'spu_id' => $spuId,
- 'sync_price_info' => $syncPrices,
- 'sync_title' => $syncData['title'],
- 'sync_status' => $syncData['status'],
- 'sync_time' => date('Y-m-d H:i:s')
- ];
- // 4. 记录变动日志
- $this->recordChangeLog(
- $productId,
- $spuId,
- 'price_change',
- '第三方平台价格变动超过阈值',
- $beforeData,
- $afterData
- );
- Log::info("商品下架: product_id={$productId}, spu_id={$spuId}, 原因: 价格变动");
- }
- /**
- * 更新商品图片(基于原有updata_product_img方法)
- */
- protected function updateProductImages($spuId)
- {
- // 1. 验证该SPU是否已上架
- $productId = Db::name('store_product')
- ->where('spu_id', $spuId)
- ->value('product_id');
- if (!$productId) {
- return true;
- }
- // 2. 查找微唯宝商品
- $data = Db::name('douhuomall')
- ->where('spu_id', $spuId)
- ->find();
- if (!$data) {
- return false;
- }
- // 3. 解析商品信息
- $spuIdInfo = json_decode($data['spuId_info'], true);
- // 4. 处理图片数据
- $sliderImage = $spuIdInfo['detail_img_list'] ?? '';
- $image = $spuIdInfo['cover_url'] ?? '';
- try {
- $sliderImage = implode(',', json_decode($sliderImage, true));
- } catch (\Exception $e) {
- $sliderImage = $image;
- }
- // 5. 更新产品图片数据
- $updateData = [
- 'image' => $image,
- 'slider_image' => $sliderImage,
- ];
- Db::name('store_product')
- ->where('product_id', $productId)
- ->update($updateData);
- // 6. 更新商品详情
- $detail = $spuIdInfo['detail'] ?? '';
- if (!is_null(json_decode($detail))) {
- $detailList = json_decode($detail);
- $newDetail = '';
- foreach ($detailList as $value) {
- $newDetail .= "<img src='{$value}' referrerpolicy='no-referrer' /></img>";
- }
- $detail = $newDetail;
- }
- Db::name('store_product_content')
- ->where('product_id', $productId)
- ->update(['content' => $detail]);
- return true;
- }
- /**
- * 处理需要下架的商品(第三方已下架)
- */
- protected function handleOfflineProducts($localSpuIds, $syncedSpuIds)
- {
- $offlineSpuIds = array_diff($localSpuIds, $syncedSpuIds);
- if (empty($offlineSpuIds)) {
- return;
- }
- foreach ($offlineSpuIds as $spuId) {
- $this->offlineProductBySpuId($spuId, 'third_party_offline');
- }
- Log::info("下架第三方已下架商品", ['count' => count($offlineSpuIds), 'spu_ids' => $offlineSpuIds]);
- }
- /**
- * 根据SPU ID下架商品
- */
- protected function offlineProductBySpuId($spuId, $reason)
- {
- // 查找已上架的商品
- $products = Db::name('store_product')
- ->where('spu_id', $spuId)
- ->select();
- foreach ($products as $product) {
- // 记录下架前数据
- $beforeData = [
- 'product_id' => $product['product_id'],
- 'spu_id' => $spuId,
- 'title' => $product['store_name'] ?? '',
- 'status' => $product['is_show'] ?? 0,
- 'price' => $product['price'] ?? 0,
- 'record_time' => date('Y-m-d H:i:s')
- ];
- // 下架商品
- Db::name('store_product')
- ->where('product_id', $product['product_id'])
- ->update([
- 'is_show' => 0,
- 'offline_time' => time(),
- 'offline_reason' => $reason
- ]);
- // 记录日志
- $this->recordChangeLog(
- $product['product_id'],
- $spuId,
- 'third_party_offline',
- $reason,
- $beforeData,
- []
- );
- }
- // 从douhuomall表中删除
- Db::name('douhuomall')->where('spu_id', $spuId)->delete();
- }
- /**
- * 记录变动日志
- */
- protected function recordChangeLog($productId, $spuId, $changeType, $reason, $beforeData, $afterData)
- {
- $logData = [
- 'product_id' => $productId,
- 'spu_id' => $spuId,
- 'change_type' => $changeType,
- 'change_reason' => $reason,
- 'before_data' => json_encode($beforeData, JSON_UNESCAPED_UNICODE),
- 'after_data' => json_encode($afterData, JSON_UNESCAPED_UNICODE),
- 'change_time' => time(),
- 'create_time' => time()
- ];
- Db::name('product_change_log')->insert($logData);
- }
- /**
- * 数组排序(从原有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($platPrice, bcmul($platPrice, '0.03', 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,
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'error' => $e->getMessage()
- ];
- Log::error('商品同步错误', $errorInfo);
- Db::name('log')->insert([
- 'data' => json_encode($errorInfo, JSON_UNESCAPED_UNICODE),
- 'time' => date('Y-m-d H:i:s')
- ]);
- }
- }
|