// +---------------------------------------------------------------------- declare (strict_types=1); namespace app\job\service\goods; use app\common\library\helper; use app\common\service\BaseService; use app\common\model\Spec as SpecModel; use app\common\model\Goods as GoodsModel; use app\common\model\Category as CategoryModel; use app\common\model\GoodsSku as GoodsSkuModel; use app\common\model\Delivery as DeliveryModel; use app\common\model\UploadFile as UploadFileModel; use app\common\model\GoodsImage as GoodsImageModel; use app\common\model\goods\Import as GoodsImportModel; use app\common\model\GoodsSpecRel as GoodsSpecRelModel; use app\common\model\goods\Service as GoodsServiceModel; use app\common\model\goods\ServiceRel as GoodsServiceRelModel; use app\common\model\GoodsCategoryRel as GoodsCategoryRelModel; use app\common\enum\goods\Status as GoodsStatusEnum; use app\common\enum\goods\GoodsType as GoodsTypeEnum; use app\common\enum\goods\SpecType as GoodsSpecTypeEnum; use app\common\enum\goods\ImportStatus as GoodsImportStatusEnum; use app\common\enum\goods\DeductStockType as DeductStockTypeEnum; use app\common\validate\goods\Import as GoodsImportValidate; use cores\exception\BaseException; /** * 服务类:商品批量导入 * Class Import * @package app\job\service\goods */ class Import extends BaseService { /** * 错误日志记录 * @var array */ private array $errorLog = []; /** * 导入成功的数量 * @var int */ private int $successCount = 0; /** * 商品主信息字段映射 * @var string[] */ private array $mapGoods = [ 'goods_name' => 'B', 'goods_no' => 'C', 'categoryIds' => 'D', 'imagesIds' => 'E', 'delivery_id' => 'F', 'status' => 'G', 'sort' => 'N', 'deduct_stock_type' => 'O', 'sales_initial' => 'P', 'serviceIds' => 'Q', 'is_points_gift' => 'R', 'is_points_discount' => 'S', 'is_enable_grade' => 'T', ]; /** * 商品SKU字段映射 * @var string[] */ private array $mapSku = [ 'specText' => 'H', 'goods_price' => 'I', 'line_price' => 'J', 'stock_num' => 'K', 'goods_weight' => 'L', 'goods_sku_no' => 'M', ]; /** * 批量导入商品 * @param array $list * @param int $recordId * @param int $storeId * @return bool * @throws BaseException * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function batch(array $list, int $recordId, int $storeId): bool { foreach ($list as $item) { // 生成商品数据(用于写入数据库) $data = $this->createData($item, $storeId); // 数据验证 if (!$this->validateGoodsData($data, $storeId)) { $this->errorLog[] = ['goodsSn' => $item['A'], 'message' => $this->getError()]; continue; } // 事务处理:添加商品 $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); // 新增服务与承诺关联 GoodsServiceRelModel::increased((int)$model['goods_id'], $data['serviceIds'], $storeId); }); // 记录导入成功 $this->successCount++; } // 更新导入记录 $this->updateRecord($recordId, \count($list)); return true; } /** * 更新导入记录 * @param int $recordId 商品导入记录ID * @param int $currentCount 当前任务导入的商品总量 * @return void */ private function updateRecord(int $recordId, int $currentCount) { // 获取导入记录 $model = GoodsImportModel::detail($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' => GoodsImportStatusEnum::COMPLETED]); } } /** * 商品数据验证 * @param array $data * @param int $storeId * @return bool */ private function validateGoodsData(array $data, int $storeId): bool { // 验证商品信息:商品名称、分类ID集、图片ID集、运费模板ID $validate = new GoodsImportValidate; if (!$validate->scene('goods')->check($data)) { $this->setError($validate->getError()); return false; } // 验证SKU信息:商品价格、库存数量、商品重量 $skuList = $data['spec_type'] == GoodsSpecTypeEnum::MULTI ? $data['newSkuList'] : [$data['newSkuList']]; foreach ($skuList as $item) { $validate = new GoodsImportValidate; if (!$validate->scene('skuInfo')->check($item)) { $this->setError($validate->getError()); return false; } } // 验证运费模板ID是否存在 if (!DeliveryModel::checkDeliveryId((int)$data['delivery_id'], $storeId)) { $this->setError('运费模板ID不存在'); return false; } if ($data['spec_type'] == GoodsSpecTypeEnum::MULTI) { // 判断用户填写的SKU数量是否正确 $shouldSkuTotal = SpecModel::calcSkuListTotal($data['specData']['specList']); $originalSkuTotal = \count($data['specData']['skuList']); if ($shouldSkuTotal !== $originalSkuTotal) { $this->setError('商品SKU数量不正确'); return false; } // 判断商品SKU是否存在重复规格 $goodsSkuIdArr = helper::getArrayColumn($data['newSkuList'], 'goods_sku_id'); if (count($data['newSkuList']) != count(array_unique($goodsSkuIdArr))) { $this->setError('商品SKU存在重复的规格值'); return false; } } return true; } /** * 生成商品数据(用于写入数据库) * @param array $original * @param int $storeId * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ private function createData(array $original, int $storeId): array { // 商品规格 $specType = isset($original['skuList']) ? GoodsSpecTypeEnum::MULTI : GoodsSpecTypeEnum::SINGLE; // 整理商品数据 $data = [ 'goods_type' => GoodsTypeEnum::PHYSICAL, 'goods_name' => $original[$this->mapGoods['goods_name']], 'goods_no' => $original[$this->mapGoods['goods_no']], 'spec_type' => $specType, 'delivery_id' => $original[$this->mapGoods['delivery_id']], 'sort' => $original[$this->mapGoods['sort']], 'sales_initial' => $original[$this->mapGoods['sales_initial']], 'deduct_stock_type' => $original[$this->mapGoods['deduct_stock_type']] === '付款减库存' ? DeductStockTypeEnum::PAYMENT : DeductStockTypeEnum::CREATE, 'is_points_gift' => $original[$this->mapGoods['is_points_gift']] === '关闭' ? 0 : 1, 'is_points_discount' => $original[$this->mapGoods['is_points_discount']] === '关闭' ? 0 : 1, 'is_enable_grade' => $original[$this->mapGoods['is_enable_grade']] === '关闭' ? 0 : 1, 'status' => $original['G'] === '下架' ? GoodsStatusEnum::OFF_SALE : GoodsStatusEnum::ON_SALE, 'categoryIds' => $this->ids2array($original[$this->mapGoods['categoryIds']]), 'imagesIds' => $this->ids2array($original[$this->mapGoods['imagesIds']]), 'serviceIds' => $this->ids2array($original[$this->mapGoods['serviceIds']]), // 下面是默认数据, 没有会报错 'content' => '', 'alone_grade_equity' => [], 'newSpecList' => [], 'newSkuList' => [], 'store_id' => $storeId, ]; // 规格和sku数据处理 if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) { $specList = $this->createSpecList($original['skuList']); $data['specData']['specList'] = $specList; $data['specData']['skuList'] = $this->createSkuList($original['skuList'], $specList); // 生成多规格数据 (携带id) $data['newSpecList'] = SpecModel::getNewSpecList($data['specData']['specList'], $storeId); // 生成skuList (携带goods_sku_id) $data['newSkuList'] = GoodsSkuModel::getNewSkuList($data['newSpecList'], $data['specData']['skuList']); } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) { // 生成skuItem $data['newSkuList'] = [ 'goods_price' => $original[$this->mapSku['goods_price']], 'line_price' => $original[$this->mapSku['line_price']], 'stock_num' => $original[$this->mapSku['stock_num']], 'goods_weight' => $original[$this->mapSku['goods_weight']], // 'goods_sku_no' => $original[$this->mapSku['goods_sku_no']], ]; } // 整理商品的价格和库存总量 if ($data['spec_type'] === GoodsSpecTypeEnum::MULTI) { $data['stock_total'] = GoodsSkuModel::getStockTotal($data['specData']['skuList']); [$data['goods_price_min'], $data['goods_price_max']] = GoodsSkuModel::getGoodsPrices($data['specData']['skuList']); [$data['line_price_min'], $data['line_price_max']] = GoodsSkuModel::getLinePrices($data['specData']['skuList']); } elseif ($data['spec_type'] === GoodsSpecTypeEnum::SINGLE) { $data['goods_price_min'] = $data['goods_price_max'] = $original[$this->mapSku['goods_price']]; $data['line_price_min'] = $data['line_price_max'] = $original[$this->mapSku['line_price']]; $data['stock_total'] = $original[$this->mapSku['stock_num']]; } // 过滤不存在的ID集数据 $data['categoryIds'] = CategoryModel::filterCategoryIds($data['categoryIds'], $storeId); $data['imagesIds'] = UploadFileModel::filteFileIds($data['imagesIds'], $storeId); $data['serviceIds'] = GoodsServiceModel::filterServiceIds($data['serviceIds'], $storeId); return $data; } /** * ID集字符串转换成数组 * @param string $value * @return array|false|string[] */ private function ids2array(string $value) { return !empty($value) ? \explode(',', $value) : []; } /** * 创建标准的商品SKU数据 * @param array $originalSkuList * @param array $specList * @return array */ private function createSkuList(array $originalSkuList, array $specList): array { $data = []; foreach ($originalSkuList as $item) { // 设置skuKeys数据 foreach ($item['specNames'] as $spec) { $specGroup = helper::arraySearch($specList, 'spec_name', $spec[0]); $specValue = helper::arraySearch($specGroup['valueList'], 'spec_value', $spec[1]); $item['skuKeys'][] = [ 'groupKey' => $specGroup['key'], 'valueKey' => $specValue['key'] ]; } // 整理SKU数据 $data[] = [ 'image_id' => 0, 'goods_price' => $item[$this->mapSku['goods_price']], 'line_price' => $item[$this->mapSku['line_price']], 'stock_num' => $item[$this->mapSku['stock_num']], 'goods_weight' => $item[$this->mapSku['goods_weight']], 'goods_sku_no' => $item[$this->mapSku['goods_sku_no']], 'skuKeys' => $item['skuKeys'], ]; } return $data; } /** * 创建标准的商品规格数据(树状) * @param array $originalSkuList * @return array */ private function createSpecList(array &$originalSkuList): array { // 生成规格数据 $data = []; foreach ($originalSkuList as &$item) { $tempArr1 = \explode(',', $item['H']); // ['颜色:白色', '尺码:小'] foreach ($tempArr1 as $val) { $tempArr2 = \explode(':', $val); // ['颜色','白色'] $data[$tempArr2[0]][$tempArr2[1]] = 1; $item['specNames'][] = $tempArr2; } } // 整理为specList格式 $specList = []; foreach ($data as $specName => $specValues) { $groupKey = \count($specList); $valueList = []; foreach ($specValues as $specValue => $key) { $valueList[] = [ 'key' => \count($valueList), 'groupKey' => $groupKey, 'spec_value' => $specValue, ]; } $specList[] = [ 'key' => $groupKey, 'spec_name' => $specName, 'valueList' => $valueList, ]; } return $specList; } }