您现在的位置是:首页 > 榴榴无忌

[原创]折腾了半个月 终于把商城系统的批量导入商品的功能做出来了

| 人围观 |

沧海跨云药神2026-08-20 17:38:39

好些日子没上论坛了,这段时间光折腾代码了
起因是 有感于 手动添加商品
不仅速度慢 还要来回核对 一条一条的调整
版面还容易弄的今天一个样  明天一个样

本来想在网上找找有没有这方面的工具之类的
可找好几天也没有现成的,不是要花钱去买,就是要找人开发,干脆一狠心,自己慢慢捣鼓捣鼓吧。

先总结一下踩过的坑:
1、从数据库方面入手
主商品表连着规格表、分类表、仓库表、库存表,  规格表又连着库存表,关键是它还有个回拉动作:库存需要密钥值才能加上对应规格的库存,然后回拉到主表生产总库存和价格范围,这些搞好了后,前台购买按钮死活点不开……此路不通 

2、浏览器抓包 用批处理模拟人工提交数据
抓包后 分析了下提交的表单,按照它那个格式运行py批处理 以管理员身份向商品添加服务提交表单,结果鉴权死活不给过,模拟了各种方式,一发送就返回让登录的页面,cookie也不行……此路不通

下面总算有点眉目了
3、在服务器上用php调用它的添加接口
分析添加商品的前端页面、控制器和方法……
拿到了完整的数据格式,
一看 差不多快30个数据框了……

好吧  慢慢搞  先做个中英文对照
要不我也看不懂
複製代码
  1. '商品标题' => 'title',
  2.             '简述' => 'simple_desc',
  3.             '型号' => 'model',
  4.             '库存单位' => 'inventory_unit',
  5.             '赠送积分比例' => 'give_integral',
  6.             '分类ID' => 'category_id',
  7.             '品牌ID' => 'brand_id',
  8.             '产地省ID' => 'place_origin',
  9.             '商品类型' => 'site_type',
  10.             '是否扣减库存' => 'is_deduction_inventory',
  11.             '是否上架' => 'is_shelves',
  12.             '电脑端详情' => 'content_web',
  13.             '虚拟商品信息' => 'fictitious_goods_value',
  14.             'SEO标题' => 'seo_title',
  15.             'SEO关键词' => 'seo_keywords',
  16.             'SEO描述' => 'seo_desc',
  17.             '封面图' => 'images',
  18.             '相册图' => 'photo',
  19.             '规格名称' => 'spec_name',
  20.             '规格值' => 'spec_values',
  21.             '规格价格' => 'spec_prices',
  22.             '规格原价' => 'spec_original_prices',
  23.             '规格起购数' => 'spec_buy_min_numbers',
  24.             '规格限购数' => 'spec_buy_max_numbers',
  25.             '规格重量' => 'spec_weights',
  26.             '规格体积' => 'spec_volumes',
  27.             '规格编码' => 'spec_codings',
  28.             '规格条形码' => 'spec_barcodes',
  29.             '规格扩展' => 'spec_extends',
  30.             '仓库ID' => 'warehouse_id',
  31.             '仓库库存' => 'warehouse_inventory',
  32.             '初始销量' => 'sales_count',
  33.             '初始访问量' => 'access_count',
複製代码


然后是  弄个表  按这个格式准备内容
……………………
表弄成中文的  那些带id的  得从中文改成数字
要不咱认识  它不认识
数据库读表……加映射字,

用里面的内容去找符合的商品 挂靠id
……………………
期间py批处理各种报错  又用php  转换出错

一点一点的去调整
弄成这样应该差不多了


表准备好了  再回到我们的主线
一开始  打算用插件安装的方式添加上去
费了好几天都没装上 


原因是它有个验证机制  第三方包死活挂不上
有一次偶然通过数据库强行写入装上了

一运行又给憋回去了

哎  这个时候  跟元宝也聊了好几天了
我也发现它的问题了  太能围着你转了 
有点像万金油 啥都懂 就是解决不了问题

换了chatgpt  它建议自定义一个控制器
又弄了几天 专业是专业 但它不根据你情况去搞 老有它自己的观点  还卡对话 动不动就忘了前面的 特别轴  最终是个半成品


又换了豆包  这家伙 太能聊了 但专业知识太老旧  光给好几年前的方案
有时候一个大小写字母怎么处理都能混乱

我也累了 歇了几天

换deepseek去问
它给出的方案是
1购买导入插件
2使用erp软件
3循环调用接口添加
4直接写数据库然后回拉主表

我果断选了3 

它直接给出了调用的代码
但是也遇到了问题 
比如图片无法通过正常上传 和添加进附件记录
只能复制到服务器中返链接出来
但好在商品是可以添加上了

但添加的商品挂不上仓库
原因是它的模式习惯了接收返回值
可添加后不会有这个值返回
要根据名称主动查询
又调试了半天 
拿到了商品id 终于入库了

接下来是加库存
直接发商品id和数量去加  加不上 汗????
得查一个入库id
又折腾了一天
入库id 数量 发过去 显示添加成功
可实际没加上
梳理发现它有个加密的key
只有key 入库id 规格值 匹配上了 
又跑到服务层去找这个key怎么来的
顺便做个前端视图
每次复制代码太难受了
改成既可以命令执行  也能视图点击执行
解决了这个key的获取后  库存终于加上了

回过头来再解决那个附件记录加不上的问题
每次都提示扩展名有误  给打回来了
从图片处理层 到商品管理层 再到附件处理
就是找不到哪个程序挡回来的

最后看了下 数据格式 好像少了个点
把扩展名前面的点加上  好嘛 
附件记录添加成功

最后在看了看  有些环境引用文件不对给改改
给它加后台菜单里  方便直接调用
视图再美观优化一下,加个使用说明
再挂个模板下载按钮 扔个空模板放里面
再给前端视图添加个时间控制框
在导入的时候控制它每个完成后间隔多久
原先是每个停1秒 可以加快到每个停0.3秒
这是添加停顿时发生的BUG

也可以设置0不停顿 但服务器性能和数据库怕扛不住
前端视图效果


后端代码部分
複製代码
  1.     // ========== 命令行入口 ==========
  2.     public function 自定义方法名()
  3.     {
  4.         if (PHP_SAPI !== 'cli' && !$this->isLocalIp()) {
  5.             return json(['code' => -1, 'msg' => '禁止访问']);
  6.         }
  7.         $filePath = $this->excelFile;
  8.         if (!file_exists($filePath)) {
  9.             return json(['code' => -1, 'msg' => "Excel文件不存在:{$filePath}"]);
  10.         }
  11.         // 命令行模式默认间隔 0.3 秒
  12.         return $this->processExcelFile($filePath, 0.3);
  13.     }
複製代码

浏览器执行
複製代码
  1. public 浏览器执行方法()
  2.     {
  3.         if (empty($_FILES['excel_file']) || $_FILES['excel_file']['error'] !== 0) {
  4.             return json(['code' => -1, 'msg' => '请上传有效的 Excel 文件']);
  5.         }
  6.         $tmpFile = $_FILES['excel_file']['tmp_name'];
  7.         // 获取间隔参数,默认为 0.3 秒
  8.         $interval = isset($_POST['interval']) ? floatval($_POST['interval']) : 0.3;
  9.         if ($interval < 0) $interval = 0;
  10.         return $this->processExcelFile($tmpFile, $interval);
  11.     }
複製代码

读表部分
複製代码
  1. protected function readExcel($filePath)
  2.     {
  3.         $spreadsheet = IOFactory::load($filePath);
  4.         $sheet = $spreadsheet->getActiveSheet();
  5.         $rows = $sheet->toArray(null, true, true, true);
  6.         if (empty($rows)) {
  7.             return [];
  8.         }
  9.         $header = array_shift($rows);
  10.         $map = [
  11.             '商品标题' => 'title',
  12.             '简述' => 'simple_desc',
  13.             '型号' => 'model',
  14.             '库存单位' => 'inventory_unit',
  15.             '赠送积分比例' => 'give_integral',
  16.             '分类ID' => 'category_id',
  17.             '品牌ID' => 'brand_id',
  18.             '产地省ID' => 'place_origin',
  19.             '商品类型' => 'site_type',
  20.             '是否扣减库存' => 'is_deduction_inventory',
  21.             '是否上架' => 'is_shelves',
  22.             '电脑端详情' => 'content_web',
  23.             '虚拟商品信息' => 'fictitious_goods_value',
  24.             'SEO标题' => 'seo_title',
  25.             'SEO关键词' => 'seo_keywords',
  26.             'SEO描述' => 'seo_desc',
  27.             '封面图' => 'images',
  28.             '相册图' => 'photo',
  29.             '规格名称' => 'spec_name',
  30.             '规格值' => 'spec_values',
  31.             '规格价格' => 'spec_prices',
  32.             '规格原价' => 'spec_original_prices',
  33.             '规格起购数' => 'spec_buy_min_numbers',
  34.             '规格限购数' => 'spec_buy_max_numbers',
  35.             '规格重量' => 'spec_weights',
  36.             '规格体积' => 'spec_volumes',
  37.             '规格编码' => 'spec_codings',
  38.             '规格条形码' => 'spec_barcodes',
  39.             '规格扩展' => 'spec_extends',
  40.             '仓库ID' => 'warehouse_id',
  41.             '仓库库存' => 'warehouse_inventory',
  42.             '初始销量' => 'sales_count',
  43.             '初始访问量' => 'access_count',
  44.         ];
  45.         $mapped = [];
  46.         foreach ($header as $idx => $col) {
  47.             $col = trim($col);
  48.             if ($col && isset($map[$col])) {
  49.                 $mapped[$idx] = $map[$col];
  50.             }
  51.         }
  52.         $data = [];
  53.         foreach ($rows as $row) {
  54.             $item = [];
  55.             foreach ($mapped as $idx => $key) {
  56.                 $item[$key] = $row[$idx] ?? '';
  57.             }
  58.             if (!empty($item['title'])) {
  59.                 $data[] = $item;
  60.             }
  61.         }
  62.         return $data;
  63.     }
複製代码

核心处理部分
複製代码
  1. protected function processExcelFile($filePath, $interval = 0.3)
  2.     {
  3.         $rows = $this->readExcel($filePath);
  4.         if (empty($rows)) {
  5.             return json(['code' => -1, 'msg' => '没有有效的商品数据']);
  6.         }
  7.         $total = count($rows);
  8.         $success = 0;
  9.         foreach ($rows as $index => $row) {
  10.             $num = $index + 1;
  11.             $title = $row['title'] ?? '无标题';
  12.             echo "处理第 {$num}/{$total} 个商品:{$title}\n";
  13.             try {
  14.                 if ($this->processOneGoods($row)) {
  15.                     $success++;
  16.                 }
  17.             } catch (\Exception $e) {
  18.                 echo "异常:{$e->getMessage()}\n";
  19.             }
  20.             if ($num < $total && $interval > 0) {
  21.                 usleep($interval * 1000000);
  22.             }
  23.         }
  24.         $fail = $total - $success;
  25.         echo "\n汇总:总 {$total},成功 {$success},失败 {$fail}\n";
  26.         return json(['code' => 0, 'data' => compact('total', 'success', 'fail')]);
  27.     }
複製代码

商品添加部分
複製代码
  1. protected function processOneGoods($row)
  2.     {
  3.         // 1. 处理图片
  4.         $uploaded = $this->processImages($row);
  5.         // 2. 添加商品
  6.         $goodsData = $this->buildGoodsData($row, $uploaded);
  7.         $ret = GoodsService::GoodsSave($goodsData);
  8.         if ($ret['code'] !== 0) {
  9.             echo "  ❌ 商品添加失败:{$ret['msg']}\n";
  10.             return false;
  11.         }
  12.         // 获取商品 ID
  13.         $goodsId = 0;
  14.         if (isset($ret['data']) && is_array($ret['data']) && isset($ret['data']['id'])) {
  15.             $goodsId = $ret['data']['id'];
  16.         }
  17.         if ($goodsId <= 0) {
  18.             $params = [
  19.                 'where' => [
  20.                     ['title', '=', $goodsData['title']],
  21.                     ['add_time', '>=', time() - 60],
  22.                 ],
  23.                 'order_by' => 'id desc',
  24.                 'limit' => 1,
  25.                 'field' => 'id',
  26.             ];
  27.             $result = GoodsService::GoodsList($params);
  28.             if ($result['code'] === 0 && !empty($result['data'])) {
  29.                 $goodsId = $result['data'][0]['id'];
  30.             }
  31.         }
  32.         if ($goodsId <= 0) {
  33.             echo "  ❌ 无法获取商品ID\n";
  34.             return false;
  35.         }
  36.         echo "  ✅ 商品添加成功,ID={$goodsId}\n";
複製代码


仓库挂靠部分
複製代码
  1. $warehouseId = !empty($row['warehouse_id']) ? intval($row['warehouse_id']) : $this->defaultWarehouseId;
  2.         $wgRet = WarehouseGoodsService::WarehouseGoodsAdd(['warehouse_id' => $warehouseId, 'goods_id' => $goodsId]);
  3.         if ($wgRet['code'] !== 0) {
  4.             echo "  ❌ 仓库关联失败:{$wgRet['msg']}\n";
  5.             return false;
  6.         }
  7.         echo "  ✅ 仓库关联请求已发送\n";
  8.         // 获取仓库商品记录 ID
  9.         $wgId = Db::name('warehouse_goods')
  10.             ->where('warehouse_id', $warehouseId)
  11.             ->where('goods_id', $goodsId)
  12.             ->value('id');
  13.         if (!$wgId) {
  14.             echo "  ❌ 无法获取仓库记录ID\n";
  15.             return false;
  16.         }
  17.         echo "  ℹ️ 通过数据库查询获取到仓库记录ID:{$wgId}\n";
  18.         // 4. 状态更新
  19.         $statusRet = WarehouseGoodsService::WarehouseGoodsStatusUpdate([
  20.             'id'    => $wgId,
  21.             'field' => 'is_enable',
  22.             'state' => 1,
  23.             'admin' => $this->admin ?? ['id' => 1],
  24.         ]);
  25.         if ($statusRet['code'] !== 0) {
  26.             echo "  ⚠️ 状态更新失败:{$statusRet['msg']}\n";
  27.         } else {
  28.             echo "  ✅ 状态更新成功\n";
  29.         }
複製代码

库存保存部分 
複製代码
  1. $inventoryData = WarehouseGoodsService::WarehouseGoodsInventoryData(['id' => $wgId]);
  2.         if ($inventoryData['code'] !== 0) {
  3.             echo "  ❌ 获取库存数据失败:{$inventoryData['msg']}\n";
  4.             return false;
  5.         }
  6.         $specList = $inventoryData['data']['spec'] ?? [];
  7.         if (empty($specList)) {
  8.             echo "  ⚠️ 商品无规格,无需设置库存\n";
  9.             return true;
  10.         }
  11.         echo "  ℹ️ 库存数据获取成功,规格数量:" . count($specList) . "\n";
  12.         // 6. 准备库存保存参数
  13.         $md5Keys = array_column($specList, 'md5_key');
  14.         $specs = array_column($specList, 'spec');
  15.         // 解析 Excel 库存值(逗号分隔)
  16.         $invStr = isset($row['warehouse_inventory']) ? trim($row['warehouse_inventory']) : '';
  17.         $inventories = [];
  18.         if ($invStr === '') {
  19.             $inventories = array_fill(0, count($specList), '321');
  20.             echo "  ⚠️ Excel 未提供库存值,将全部设为 321\n";
  21.         } else {
  22.             $parts = array_map('trim', explode(',', $invStr));
  23.             while (count($parts) < count($specList)) {
  24.                 $parts[] = '0';
  25.             }
  26.             $inventories = array_slice($parts, 0, count($specList));
  27.         }
  28.         // 调用保存库存
  29.         $saveParams = [
  30.             'id' => $wgId,
  31.             'specifications_inventory' => $inventories,
  32.             'specifications_md5_key' => $md5Keys,
  33.             'specifications_spec' => $specs,
  34.         ];
  35.         $saveRet = WarehouseGoodsService::WarehouseGoodsInventorySave($saveParams);
  36.         if ($saveRet['code'] !== 0) {
  37.             echo "  ❌ 库存设置失败:{$saveRet['msg']}\n";
  38.             return false;
  39.         } else {
  40.             echo "  ✅ 库存设置成功(返回信息:{$saveRet['msg']})\n";
  41.         }
  42.         return true;
  43.     }
複製代码

我感觉  这个库存处理是最复杂的了
其他的 图片处理 文件复制之类的就不贴了
有需要的可以给我发私信 无偿发给

这是今天早上的操作

前端点击  它要等到后端全部处理完成后才会刷新

感谢您的观看!
随手一赞  手留余香!

随便看看