// +---------------------------------------------------------------------- declare (strict_types=1); namespace app\job\service\goods; use app\store\service\Upload as UploadService; use app\common\service\BaseService; use app\common\model\Spec as SpecModel; use app\common\model\Goods as GoodsModel; use app\common\model\GoodsSku as GoodsSkuModel; use app\common\model\GoodsImage as GoodsImageModel; use app\common\model\store\Setting as SettingModel; use app\common\model\GoodsSpecRel as GoodsSpecRelModel; use app\common\model\goods\Collector as GoodsCollectorModel; use app\common\model\GoodsCategoryRel as GoodsCategoryRelModel; use app\common\enum\Setting as SettingEnum; use app\common\enum\file\FileType as FileTypeEnum; use app\common\enum\goods\SpecType as GoodsSpecTypeEnum; use app\common\enum\goods\DeductStockType as DeductStockTypeEnum; use app\common\enum\goods\CollectorStatus as GoodsCollectorStatusEnum; use app\common\library\helper; use app\common\library\Download; use app\common\library\collector\tools\UrlStore; use app\common\library\collector\tools\UrlItemId; use app\common\library\collector\Facade as CollectorFacade; use cores\exception\BaseException; /** * 服务类:商品批量采集 * Class Collector * @package app\job\service\goods */ class Collector extends BaseService { /** * 商品采集记录 * @var GoodsCollectorModel|null */ private ?GoodsCollectorModel $record = null; /** * 错误日志记录 * @var array */ private array $errorLog = []; /** * 采集成功的数量 * @var int */ private int $successCount = 0; /** * 图片url临时记录 (同一个网络图片仅入库一次) * @var array */ private array $dictImageUrls = []; /** * 批量采集商品 * @param array $urls 采集的商品url集 * @param array $form 用户提交的表单 * @param int $recordId 采集记录id * @param int $storeId 当前商城id * @return bool * @throws BaseException * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function batch(array $urls, array $form, int $recordId, int $storeId): bool { // 检查采集记录状态是否异常 if (!$this->checkStatusFail($recordId)) { foreach ($urls as $url) { try { // 采集第三方商品数据 $original = $this->collector($url, $storeId); // 下载远程商品图片 $original = $this->thirdPartyImages($original, $form['imageStorage'], $storeId); } catch (\Throwable $e) { tre($e->getTraceAsString()); $this->errorLog[] = ['url' => trim($url), 'message' => $e->getMessage()]; continue; } // 生成商品数据(用于写入数据库) $data = $this->createData($original, $form, $storeId); // 事务处理:添加商品 $model = new GoodsModel(); $model->transaction(function () use ($model, $data, $storeId) { // 添加商品 $model->save($data); // 新增商品与分类关联 GoodsCategoryRelModel::increased((int)$model['goods_id'], $data['categoryIds'], $storeId); // 新增商品与图片关联 GoodsImageModel::increased((int)$model['goods_id'], $data['imagesIds'], $storeId); // 新增商品与规格关联 GoodsSpecRelModel::increased((int)$model['goods_id'], $data['newSpecList'], $storeId); // 新增商品sku信息 GoodsSkuModel::add((int)$model['goods_id'], $data['spec_type'], $data['newSkuList'], $storeId); }); // 记录采集成功 $this->successCount++; } } // 更新采集记录 $this->updateRecord($recordId, \count($urls)); return true; } public function single(string $url, array $form, int $storeId): bool { try { // 采集第三方商品数据 $original = $this->collector($url, $storeId); // 下载远程商品图片 $original = $this->thirdPartyImages($original, $form['imageStorage'], $storeId); } catch (\Throwable $e) { // var_dump($e->getMessage()); // exit; tre($e->getTraceAsString()); $this->errorLog[] = ['url' => trim($url), 'message' => $e->getMessage()]; return false; } // 生成商品数据(用于写入数据库) $data = $this->createData($original, $form, $storeId); // 事务处理:添加商品 $model = new GoodsModel(); $model->transaction(function () use ($model, $data, $storeId) { // 添加商品 $model->save($data); // 新增商品与分类关联 GoodsCategoryRelModel::increased((int)$model['goods_id'], $data['categoryIds'], $storeId); // 新增商品与图片关联 GoodsImageModel::increased((int)$model['goods_id'], $data['imagesIds'], $storeId); // 新增商品与规格关联 GoodsSpecRelModel::increased((int)$model['goods_id'], $data['newSpecList'], $storeId); // 新增商品sku信息 GoodsSkuModel::add((int)$model['goods_id'], $data['spec_type'], $data['newSkuList'], $storeId); }); return true; } /** * 检查采集记录状态是否异常 * @param int $recordId * @return bool */ private function checkStatusFail(int $recordId): bool { $model = $this->getRecord($recordId); return $model['status'] == GoodsCollectorStatusEnum::FAIL || $model['is_delete']; } /** * 抓取第三方商品内容 * @param string $url * @param int $storeId * @return array * @throws BaseException * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ private function collector(string $url, int $storeId): array { // 获取商品来源 $store = $this->getStore($url); // 获取url中的商品ID $itemId = $this->getItemId($url, $store); // 商城采集设置 $config = SettingModel::getItem(SettingEnum::COLLECTOR, $storeId); // 请求API查询商品详情 return CollectorFacade::store($store) ->setOptions($config['config'][$config['provider']]) ->detail($itemId); } /** * 根据url获取商品来源 * @param string $url * @return string * @throws BaseException */ private function getStore(string $url): string { $UrlStore = new UrlStore($url); if (!$store = $UrlStore->getStore()) { throwError('不支持该网址的采集,请检查url是否正确'); } return $store; } /** * 获取url中的商品ID * @param string $url * @param string $store * @return string * @throws BaseException */ private function getItemId(string $url, string $store): string { $UrlItemId = new UrlItemId($url, $store); if (!$itemId = $UrlItemId->getItemId()) { throwError('未能获取到商品ID,请检查url是否正确'); } return $itemId; } /** * 更新采集记录 * @param int $recordId 商品采集记录ID * @param int $currentCount 当前任务采集的商品总量 * @return void */ private function updateRecord(int $recordId, int $currentCount) { // 获取采集记录 $model = $this->getRecord($recordId); // 计算采集失败的数量 $failCount = $currentCount - $this->successCount; // 更新采集记录 $model->save([ 'success_count' => $model['success_count'] + $this->successCount, 'fail_count' => $model['fail_count'] + $failCount, 'fail_log' => array_merge($model['fail_log'], $this->errorLog), ]); // 判断是否为最后一次队列 if ($model['total_count'] <= ($model['success_count'] + $model['fail_count'])) { $model->save(['end_time' => \time(), 'status' => GoodsCollectorStatusEnum::COMPLETED]); } } /** * 获取采集记录 * @param int $recordId 商品采集记录ID * @return GoodsCollectorModel|array|null */ private function getRecord(int $recordId) { if (!$this->record) { $this->record = GoodsCollectorModel::detail($recordId); } return $this->record; } /** * 生成商品数据(用于写入数据库) * @param array $original 商品原始数据 * @param array $form 用户提交的表单 * @param int $storeId 当前商城id * @return array * @throws BaseException * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ private function createData(array $original, array $form, int $storeId): array { // 整理商品数据 $data = [ 'goods_type' => $form['goods_type'], 'goods_name' => $original['goods_name'], 'spec_type' => $original['spec_type'], 'delivery_id' => $form['delivery_id'] ?? 0, 'content' => $original['content'] ?? '', 'sort' => 100, 'deduct_stock_type' => DeductStockTypeEnum::CREATE, 'status' => $form['goods_status'], 'imagesIds' => $original['imagesIds'], 'categoryIds' => $form['categoryIds'], // 下面是默认数据, 没有会报错 'alone_grade_equity' => [], 'newSpecList' => [], 'newSkuList' => [], 'store_id' => $storeId, ]; if (isset($form['channel'])) { $data['channel'] = $form['channel']; } // 整理商品的价格和库存总量 if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) { $data['stock_total'] = GoodsSkuModel::getStockTotal($original['specData']['skuList']); [$data['goods_price_min'], $data['goods_price_max']] = GoodsSkuModel::getGoodsPrices($original['specData']['skuList']); [$data['line_price_min'], $data['line_price_max']] = GoodsSkuModel::getLinePrices($original['specData']['skuList']); } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) { $data['goods_price_min'] = $data['goods_price_max'] = $original['goods_price']; $data['line_price_min'] = $data['line_price_max'] = $original['line_price']; $data['stock_total'] = $original['stock_num']; } // 规格和sku数据处理 if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) { // 验证规格值是否合法 SpecModel::checkSpecData($original['specData']['specList']); // 生成多规格数据 (携带id) $data['newSpecList'] = SpecModel::getNewSpecList($original['specData']['specList'], $storeId); // 生成skuList (携带goods_sku_id) $data['newSkuList'] = GoodsSkuModel::getNewSkuList($data['newSpecList'], $original['specData']['skuList']); } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) { // 生成skuItem $data['newSkuList'] = helper::pick($original, ['goods_price', 'line_price', 'stock_num', 'goods_weight']); } return $data; } /** * 下载远程商品图片 * @param array $original 商品信息 * @param int $imageStorage 商品主图 (10下载到本地、20使用源图片url) * @param int $storeId * @return array * @throws BaseException * @throws \think\Exception * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ private function thirdPartyImages(array $original, int $imageStorage, int $storeId): array { // 保存商品主图 $original['imagesIds'] = []; foreach ($original['goodsImages'] as $imageUrl) { $original['imagesIds'][] = $imageStorage === 10 ? $this->downImage($imageUrl, $storeId) : $this->externalImage($imageUrl, $storeId); } // 保存商品SKU封面图 if ($original['spec_type'] === GoodsSpecTypeEnum::MULTI) { foreach ($original['specData']['skuList'] as &$skuItem) { if (!empty($skuItem['imageUrl'])) { $skuItem['image_id'] = $imageStorage === 10 ? $this->downImage($skuItem['imageUrl'], $storeId) : $this->externalImage($skuItem['imageUrl'], $storeId); } } } return $original; } /** * 将远程图片外链保存到文件库中 * @param string $imageUrl 远程图片url * @param int $storeId * @return mixed * @throws BaseException */ private function externalImage(string $imageUrl, int $storeId) { // 如果已存在该文件则直接返回文件ID if (isset($this->dictImageUrls[$imageUrl])) { return $this->dictImageUrls[$imageUrl]; } // 文件外链记录到文件库 $UploadService = (new UploadService)->setStoreId($storeId); if (!$UploadService->uploadByExternal(FileTypeEnum::IMAGE, $imageUrl)) { throwError('图片记录失败:' . $UploadService->getError()); } // 记录文件ID $fileId = $UploadService->getFileInfo()['file_id']; $this->dictImageUrls[$imageUrl] = $fileId; return $fileId; } /** * 将远程图片下载并保存到文件库中 * @param string $imageUrl 远程图片url * @param int $storeId * @return mixed * @throws BaseException * @throws \think\Exception * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ private function downImage(string $imageUrl, int $storeId) { // 如果已存在该文件则直接返回文件ID if (isset($this->dictImageUrls[$imageUrl])) { return $this->dictImageUrls[$imageUrl]; } // 下载远程图片 $filePath = (new Download)->saveTempImage($storeId, $imageUrl); // 文件上传到文件库 $UploadService = (new UploadService)->setStoreId($storeId); if (!$UploadService->uploadByLocal(FileTypeEnum::IMAGE, $filePath)) { throwError('图片上传失败:' . $UploadService->getError()); } // 记录文件ID $fileId = $UploadService->getFileInfo()['file_id']; $this->dictImageUrls[$imageUrl] = $fileId; return $fileId; } }