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, '1.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,
'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++;
}
}
}
}