Files
bsh_sample/app/admin/controller/SkuController.php
T
2025-12-16 15:10:32 +08:00

495 lines
16 KiB
PHP
Raw 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){
$data['spec'][$k]['attr_type'] = SkuAttribute::where('id', $v['attr_id'])->value('attr_type');
}
}
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)
{
$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);
$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 = [];
foreach ($header as $i => $field) {
if (in_array($field, $allowFields)) {
$data[$fieldsName[$field]] = trim((string)($rowData[$i] ?? ''));
}
}
if (empty($data['product_name'])) {
continue;
}
$data['category_id'] = PostCategory::where('name', $rowData[3])->value('id')??0;
//商品属性
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[6])){
$tmpPath = runtime_path('upload/import/pic/'.$rowData[6]);
$res = $uploadService->uploadLocalFile(['path' => $tmpPath, 'scene' => 'sku']);
$data['main_image'] = $res['file_name'];
}
//详情
if(!empty($rowData[7])){
$docxFullPath = runtime_path('upload/import/docs/'.$rowData[7]);
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['file_url'].'"', $m[0]);
}
}
return $m[0];
},
$html
);
// // 2️⃣ 关键:HTML 清洗(适配 wangEditor)
// $html = $this->cleanWordHtmlForWangEditor($html);
// 4. 清洗 HTML(关键)
$html = $this->cleanDocHtml($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);
}
/**
* 清洗 doc/docx 转出的 HTML,适配富文本编辑器
*/
function cleanDocHtml(string $html): string
{
libxml_use_internal_errors(true);
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadHTML(
'<?xml encoding="UTF-8">' . $html,
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
$xpath = new DOMXPath($dom);
// 1. 删除 head / meta / style / script
foreach (['head', 'meta', 'style', 'script', 'xml'] as $tag) {
foreach ($xpath->query("//{$tag}") as $node) {
$node->parentNode?->removeChild($node);
}
}
// 2. 只取 body 内容
$body = $dom->getElementsByTagName('body')->item(0);
if ($body) {
$innerHtml = '';
foreach ($body->childNodes as $child) {
$innerHtml .= $dom->saveHTML($child);
}
} else {
$innerHtml = $dom->saveHTML();
}
libxml_clear_errors();
return trim($innerHtml);
}
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.xlsx';
if (!file_exists($filePath)) {
return response('文件不存在', 404);
}
return response()->download(
$filePath,
'导入示例.xlsx' // 下载时显示的文件名
);
}
}