Files
2025-12-31 15:56:47 +08:00

630 lines
21 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\model\PostCategory;
use app\common\model\Sku;
use app\common\model\SkuAttribute;
use app\common\service\OssService;
use app\common\service\UploadService;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\SkuService;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use support\Db;
use support\Request;
use support\Response;
use ZipArchive;
class SkuController extends BaseController
{
use CrudControllerTrait;
protected $noNeedLogin = ['template'];
protected $noNeedHeaderData = ['template'];
protected $service;
public function __construct()
{
$this->service = new SkuService();
}
/**
* 创建
*
* @param Request $request 请求对象
* @return mixed 返回创建成功的数据或信息
*/
public function store(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
$res = $this->service->serviceStore($request, $data);
return $this->success($res, '创建成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['spec_json'])){
$data['spec'] = json_decode($data['spec_json'], true);
foreach ($data['spec'] as $k=>$v){
$skuAttribute = SkuAttribute::where('category_id',$data['category_id'])
->where('attr_name', $v['attr_name'])->first();
if ($skuAttribute){
$data['spec'][$k]['attr_id'] = $skuAttribute['id'];
$data['spec'][$k]['attr_type'] = $skuAttribute['attr_type'];
}else{
$data['spec'][$k]['attr_type'] = SkuAttribute::where('id', $v['attr_id'])->value('attr_type');
}
if($data['spec'][$k]['attr_type'] == 'multiselect'){
$data['spec'][$k]['attr_value'] = str_replace([',', ',', ' '], ',', $v['attr_value']);
}
}
}
if (isset($data['category_id']))
$data['category_name'] = PostCategory::where('id', $data['category_id'])->value('name');
$ossservice = new OssService();
if (!empty($data['image'])){
$data['image'] = explode(',',$data['image']);
foreach ($data['image'] as $key => $image){
$data['image_pull'][$key] = $ossservice->generatePrivateUrl($image);
}
}else{
$data['image'] = [];
$data['image_pull'] = [];
}
if (!empty($data['main_image'])){
$data['main_image_pull'] = $ossservice->generatePrivateUrl($data['main_image']);
} else {
$data['main_image_pull'] = [];
}
return $data;
}
public function import(Request $request)
{
// ini_set('memory_limit', '512M');
$file = $request->file('file');
if (!$file) return $this->error('未上传文件');;
$zipPath = runtime_path('upload/' . uniqid() . '.zip');
$file->move($zipPath);
$extractTo = runtime_path('upload/import/');
clearstatcache(true, $zipPath);
$this->unzipPackage($zipPath,$extractTo);
$path = runtime_path('upload/import/product.xlsx');
$spreadsheet = IOFactory::load($path);
$sheet = $spreadsheet->getActiveSheet();
// 提取图片(按行号)
//$rowImages = $this->extractImages($sheet);
$highestRow = $sheet->getHighestRow();
$highestCol = $sheet->getHighestColumn();
$header = $sheet->rangeToArray("A1:{$highestCol}1")[0];
// $header = array_map('trim', $header);
$allowFields = [
'product_name' => '产品名称',
'model' => '产品型号',
'brand' => '品牌',
'category_id' => '分类',
'price' => '价格',
//'spec_json' => '商品属性'
];
$fieldsName = array_flip($allowFields);
$attributeFields = [
'外观' => '外观',
'产品尺寸(宽*高*深)' => '产品尺寸(宽*高*深)',
'开孔尺寸(宽*高*深)' => '开孔尺寸(宽*高*深)',
'容积' => '容积',
'产品卖点' => '产品卖点',
];
$attributeFieldsName = array_flip($attributeFields);
$insertData = [];
$now = time();
Db::beginTransaction();
$uploadService = new UploadService();
try {
for ($row = 2; $row <= $highestRow; $row++) {
$rowData = $sheet->rangeToArray(
"A{$row}:{$highestCol}{$row}", null, true, false
)[0];
$data = [];
$attributes = [];
foreach ($header as $i => $field) {
if (in_array($field, $allowFields)) {
$data[$fieldsName[$field]] = trim((string)($rowData[$i] ?? ''));
}
//商品属性
if (in_array($field, $attributeFields) && !empty($rowData[$i])) {
$attributes[$attributeFieldsName[$field]] = trim((string)($rowData[$i] ?? ''));
}
}
if (empty($data['product_name'])) {
continue;
}
$data['category_id'] = PostCategory::where('name', $rowData[4])->value('id')??0;
$spec = [];
foreach ($attributes as $key => $attribute) {
$skuAttribute = SkuAttribute::where('category_id',$data['category_id'])
->where('attr_name', $key)
->first();
if (empty($skuAttribute)) {
$skuAttribute = SkuAttribute::create([
'category_id' => $data['category_id'],
'attr_name' => $key,
'attr_type' => 'text',
]);
}
$attr = ['attr_name' => $key,'attr_value'=> $attribute];
$attr['attr_id'] = $skuAttribute->id;
$attr['category_id'] = $data['category_id'];
// $spec = ['sku_id' => $skuAttribute->id];
$spec[] = $attr;
}
$data['spec_json'] = json_encode($spec);
//商品属性
// if(!empty($rowData[5])){
// $speci = explode(';', $rowData[5]);
// $spec = [];
// foreach ($speci as $k => $v){
// $item = explode(':', $v);
// if (count($item) != 2) {
// continue;
// }
// $attr = ['attr_name'=>$item[0],'attr_value'=>$item[1]];
// $skuAttribute = SkuAttribute::where('attr_name', $item[0])
// ->where('category_id', $data['category_id'])->first();
// if (empty($skuAttribute)) {
// $skuAttribute = SkuAttribute::create([
// 'attr_name' => $item[0],
// 'attr_value' => $item[1],
// 'category_id' => $data['category_id'],
// 'attr_type' => 'text'
// ]);
// }
// $attr['attr_id'] = $skuAttribute->id;
// $attr['category_id'] = $data['category_id'];
// $spec[] = $attr;
// $data['spec_json'] = json_encode($spec);
// }
// }
//主图
if(!empty($rowData[7])){
$tmpPath = runtime_path('upload/import/pic/'.$rowData[7]);
$res = $uploadService->uploadLocalFile(['path' => $tmpPath, 'scene' => 'sku']);
$data['main_image'] = $res['file_name'];
}
//详情
if(!empty($rowData[8])){
$docxFullPath = runtime_path('upload/import/docs/'.$rowData[8]);
if (filesize($docxFullPath) !== 0 && file_exists($zipPath)) {
$data['desc'] =$this->docxToHtmlWithImages($docxFullPath);
}
//$data['desc'] =$this->docxToHtmlWithImages($docxFullPath);
}
$data['code'] = $this->service->generateUniqueCode();
$data['brand'] = $data['brand'] ?: '西门子';
$data['price'] = $data['price'] ?: 0;
$data['status'] = $data['status'] ?? 1;
// 图片
//$data['main_image'] = $rowImages[$row] ?? null;
$data['created_at'] = $now;
$data['updated_at'] = $now;
$insertData[] = $data;
if (count($insertData) >= 200) {
Sku::insert($insertData);
// Db::table('sku')->insert($insertData);
$insertData = [];
}
}
@unlink($zipPath);
if ($insertData) {
Sku::insert($insertData);
// Db::table('sku')->insert($insertData);
}
Db::commit();
} catch (\Throwable $e) {
Db::rollBack();
return $this->error($e->getMessage().$e->getLine().$e->getFile());
} finally {
@unlink($path);
}
return $this->success([], '导入成功');
}
/**
* 解压 zip 文件
*
* @param string $zipPath zip 文件绝对路径
* @param string $extractTo 解压目录
* @return bool
*/
function unzipPackage(string $zipPath, string $extractTo): bool
{
if (!file_exists($zipPath)) {
throw new \Exception('ZIP 文件不存在');
}
if (!is_dir($extractTo)) {
mkdir($extractTo, 0755, true);
}
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
throw new \Exception('ZIP 文件打开失败');
}
$zip->extractTo($extractTo);
$zip->close();
return true;
}
/**
* docx 转 HTML(富文本可用,图片上传 OSS)
*/
function docxToHtmlWithImages(string $docxPath): string
{
if (!is_file($docxPath)) {
return '';
}
$phpWord = \PhpOffice\PhpWord\IOFactory::load($docxPath);
$tmpDir = runtime_path('tmp/' . uniqid());
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
$tmpHtml = $tmpDir . '/index.html';
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');
$writer->save($tmpHtml);
$html = file_get_contents($tmpHtml);
// 1️⃣ 上传图片到 OSS
$uploadService = new UploadService();
$html = preg_replace_callback(
'/<img[^>]+src="([^"]+)"[^>]*>/i',
function ($m) use ($tmpDir, $uploadService) {
$src = $m[1];
if (!str_starts_with($src, 'http')) {
$localImg = $tmpDir . '/' . ltrim($src, './');
if (is_file($localImg)) {
$ossUrl = $uploadService->uploadLocalFile([
'path' => $localImg,
'scene' => 'desc'
]);
return preg_replace('/src="[^"]+"/', 'src="'.$ossUrl.'"', $m[0]);
}
}
return $m[0];
},
$html
);
// 2️⃣ 关键:HTML 清洗(适配 wangEditor)
$html = $this->cleanWordHtmlForWangEditor($html);
$this->deleteDir($tmpDir);
return $html;
}
function cleanWordHtmlForWangEditor(string $html): string
{
// 1. 移除 Word 专属标签
$html = preg_replace('/<o:p>.*?<\/o:p>/is', '', $html);
$html = preg_replace('/<\/?xml[^>]*>/i', '', $html);
$html = preg_replace('/<\/?v:[^>]*>/i', '', $html);
// 2. 移除 class / style
$html = preg_replace('/\sclass="[^"]*"/i', '', $html);
$html = preg_replace('/\sstyle="[^"]*"/i', '', $html);
// 3. 清理 span(保留内容)
while (preg_match('/<span[^>]*>(.*?)<\/span>/is', $html)) {
$html = preg_replace('/<span[^>]*>(.*?)<\/span>/is', '$1', $html);
}
// 4. 转换 div → p
$html = preg_replace('/<div[^>]*>/i', '<p>', $html);
$html = preg_replace('/<\/div>/i', '</p>', $html);
// 5. img 只保留 src
$html = preg_replace('/<img[^>]*src="([^"]+)"[^>]*>/i', '<img src="$1">', $html);
// 6. table 清洗
$html = preg_replace('/<table[^>]*>/i', '<table>', $html);
$html = preg_replace('/<td[^>]*>/i', '<td>', $html);
$html = preg_replace('/<th[^>]*>/i', '<th>', $html);
// 7. 去除多余空白
$html = preg_replace('/\s+/u', ' ', $html);
$html = str_replace(['<p></p>', '<p> </p>'], '', $html);
// 只保留 body 内容
if (preg_match('/<body[^>]*>(.*?)<\/body>/is', $html, $m)) {
$html = $m[1];
}
return trim($html);
}
function cleanWordHtml(string $html): string
{
// 删除 Word 注释
$html = preg_replace('/<!--(.|\s)*?-->/', '', $html);
// 删除 style / meta / link
$html = preg_replace('/<(style|meta|link)[^>]*>.*?<\/\1>/is', '', $html);
// 删除 xml 标签
$html = preg_replace('/<xml>.*?<\/xml>/is', '', $html);
// 删除 mso-xxx 样式
$html = preg_replace('/\s*mso-[^:]+:[^;"]+;?/i', '', $html);
// 清理 class="Mso..."
$html = preg_replace('/class="Mso[^"]*"/i', '', $html);
// 删除空 span
$html = preg_replace('/<span[^>]*>\s*<\/span>/i', '', $html);
// 删除 Office 条件注释
$html = preg_replace('/<!\[if.*?\]>.*?<!\[endif\]>/is', '', $html);
// 只保留 body 内容
if (preg_match('/<body[^>]*>(.*?)<\/body>/is', $html, $m)) {
$html = $m[1];
}
//限制图片宽度(防止编辑器撑爆)
$html = preg_replace(
'/<img([^>]+)>/i',
'<img$1 style="max-width:100%;">',
$html
);
return $html;
}
/**
* docx 转 HTML(富文本可用,图片上传 OSS)
*/
/*function docxToHtmlWithImages(string $docxPath): string
{
if (!is_file($docxPath)) {
return '';
}
$phpWord = \PhpOffice\PhpWord\IOFactory::load($docxPath);
$tmpDir = runtime_path('tmp/' . uniqid());
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
$tmpHtml = $tmpDir . '/index.html';
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');
$writer->save($tmpHtml);
$html = file_get_contents($tmpHtml);
// 1️⃣ 上传图片到 OSS
$uploadService = new UploadService();
$html = preg_replace_callback(
'/<img[^>]+src="([^"]+)"[^>]*>/i',
function ($m) use ($tmpDir, $uploadService) {
$src = $m[1];
if (!str_starts_with($src, 'http')) {
$localImg = $tmpDir . '/' . ltrim($src, './');
if (is_file($localImg)) {
$ossUrl = $uploadService->uploadLocalFile([
'path' => $localImg,
'scene' => 'desc'
]);
return preg_replace('/src="[^"]+"/', 'src="'.$ossUrl['file_url'].'"', $m[0]);
}
}
return $m[0];
},
$html
);
// // 2️⃣ 关键:HTML 清洗(适配 wangEditor)
$html = $this->cleanWordHtmlForWangEditor($html);
return $html;
}*/
function deleteDir(string $dir): void
{
if (!is_dir($dir)) return;
foreach (scandir($dir) as $file) {
if ($file === '.' || $file === '..') continue;
$path = $dir . '/' . $file;
is_dir($path) ? $this->deleteDir($path) : @unlink($path);
}
@rmdir($dir);
}
/*function cleanWordHtmlForWangEditor(string $html): string
{
// 1. 移除 Word 专属标签
$html = preg_replace('/<o:p>.*?<\/o:p>/is', '', $html);
$html = preg_replace('/<\/?xml[^>]*>/i', '', $html);
$html = preg_replace('/<\/?v:[^>]*>/i', '', $html);
// 2. 移除 class / style
$html = preg_replace('/\sclass="[^"]*"/i', '', $html);
$html = preg_replace('/\sstyle="[^"]*"/i', '', $html);
// 3. 清理 span(保留内容)
while (preg_match('/<span[^>]*>(.*?)<\/span>/is', $html)) {
$html = preg_replace('/<span[^>]*>(.*?)<\/span>/is', '$1', $html);
}
// 4. 转换 div → p
$html = preg_replace('/<div[^>]*>/i', '<p>', $html);
$html = preg_replace('/<\/div>/i', '</p>', $html);
// 5. img 只保留 src
$html = preg_replace('/<img[^>]*src="([^"]+)"[^>]*>/i', '<img src="$1">', $html);
// 6. table 清洗
$html = preg_replace('/<table[^>]*>/i', '<table>', $html);
$html = preg_replace('/<td[^>]*>/i', '<td>', $html);
$html = preg_replace('/<th[^>]*>/i', '<th>', $html);
// 7. 去除多余空白
$html = preg_replace('/\s+/u', ' ', $html);
$html = str_replace(['<p></p>', '<p> </p>'], '', $html);
return trim($html);
}*/
/**
* docx 转 HTML(自动上传图片到 OSS)
*
* @param string $docxPath docx 文件绝对路径
* @return string HTML
*/
/*function docxToHtmlWithImages(string $docxPath): string
{
if (!is_file($docxPath)) {
return '';
}
// 1. 读取 docx
$phpWord = WordIOFactory::load($docxPath);
// 2. 输出 HTML 到临时文件
$tmpHtml = runtime_path('tmp/' . uniqid() . '.html');
if (!is_dir(dirname($tmpHtml))) {
mkdir(dirname($tmpHtml), 0755, true);
}
$writer = WordIOFactory::createWriter($phpWord, 'HTML');
$writer->save($tmpHtml);
$html = file_get_contents($tmpHtml);
$uploadService = new UploadService();
// 3. 处理 HTML 中的图片
$html = preg_replace_callback(
'/<img[^>]+src="([^"]+)"[^>]*>/i',
function ($matches) use ($tmpHtml,$uploadService) {
$src = $matches[1];
// docx 导出的图片一般是相对路径
if (!str_starts_with($src, 'http')) {
$localImg = dirname($tmpHtml) . '/' . $src;
if (is_file($localImg)) {
// 上传到 OSS
$ossUrl = $uploadService->uploadLocalFile(['path' => $localImg, 'scene' => 'desc']);
return str_replace($src, $ossUrl, $matches[0]);
}
}
return $matches[0];
},
$html
);
// 清理临时文件
@unlink($tmpHtml);
return $html;
}*/
private function extractImages($sheet): array
{
$images = [];
$uploadService = new UploadService();
foreach ($sheet->getDrawingCollection() as $drawing) {
if ($drawing instanceof Drawing) {
// 取行号
preg_match('/\d+/', $drawing->getCoordinates(), $m);
$row = (int)$m[0];
// Excel 临时图片路径
$tmpPath = $drawing->getPath();
if (!is_file($tmpPath)) {
continue;
}
// 上传到 OSS
$ossUrl = $uploadService->uploadLocalFile(['path' => $tmpPath, 'scene' => 'sku']);
// 一行只保留一张主图
$images[$row] = $ossUrl['file_url'];
}
}
return $images;
}
/**
* 下载 Excel 示例模板
*/
public function template(Request $request): Response
{
$filePath = runtime_path() . '/upload/import.zip';
if (!file_exists($filePath)) {
return response('文件不存在', 404);
}
return response()->download(
$filePath,
'导入示例.zip' // 下载时显示的文件名
);
}
}