success('Material'); } /** * 上传图片保存扩展名:优先客户端文件名,无后缀时用 MIME 推断,避免生成 xxx. 无扩展名 */ protected function resolveUploadedImageExt($file) { $name = $file->getInfo('name') ?? ''; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if ($ext === 'jpeg') { $ext = 'jpg'; } $allowed = ['jpg', 'png', 'gif', 'webp', 'bmp']; if ($ext && in_array($ext, $allowed, true)) { return $ext; } $mime = method_exists($file, 'getMime') ? strtolower((string) $file->getMime()) : ''; $map = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp' => 'bmp', ]; if (isset($map[$mime])) { return $map[$mime]; } return 'jpg'; } /** * 根据 material_url 删除 public 下对应文件(如 uploads/material/2026-03-25/xxx.png) */ protected function unlinkMaterialFileByUrl($materialUrl) { if ($materialUrl === null || $materialUrl === '') { return; } $rel = str_replace('\\', '/', trim((string) $materialUrl)); $rel = ltrim($rel, '/'); if ($rel === '') { return; } $fullPath = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/' . $rel; if (is_file($fullPath)) { @unlink($fullPath); } } /** * 图层所属页码(多页模版):0 起,缺省或非数字按 0 */ protected function layerPageIndex(array $layer): int { if (!array_key_exists('page_index', $layer)) { return 0; } $v = $layer['page_index']; if ($v === '' || $v === null) { return 0; } if (is_numeric($v)) { return (int) $v; } return 0; } /** * 解析 product_template.page_image_urls(JSON 数组) */ protected function decodePageImageUrlsField($stored): array { if ($stored === null || $stored === '') { return []; } if (is_array($stored)) { return $stored; } if (!is_string($stored)) { return []; } $decoded = json_decode($stored, true); return is_array($decoded) ? $decoded : []; } /** * 删除多页预览图(本地 + OSS),用于更新前清理或删模版 */ protected function deleteStoredPageGalleryImages($pageImageUrlsJsonOrArray): void { $list = is_array($pageImageUrlsJsonOrArray) ? $pageImageUrlsJsonOrArray : $this->decodePageImageUrlsField($pageImageUrlsJsonOrArray); foreach ($list as $p) { if ($p === null || $p === '') { continue; } $this->unlinkMaterialFileByUrl((string) $p); Common::deleteOssObject((string) $p); } } /** * 将 preview_images[] 中每页 dataURL 落盘并同步 OSS,返回与下标对齐的路径数组(失败页为空串占位) * * @param array $previewImages 下标 0=第 1 页… * @param string $saveDir public/uploads/template/YYYY-mm-dd/ * @param string $dateYmd YYYY-mm-dd * @return array{paths: string[], oss: bool[]} */ protected function savePreviewGalleryBase64List(array $previewImages, string $saveDir, string $dateYmd): array { if ($previewImages === []) { return ['paths' => [], 'oss' => []]; } $keys = array_keys($previewImages); $maxIdx = $keys === [] ? -1 : max(array_map('intval', $keys)); $n = $maxIdx >= 0 ? ($maxIdx + 1) : 0; $paths = array_fill(0, $n, ''); $oss = array_fill(0, $n, false); foreach ($previewImages as $pageIndex => $base64Data) { $i = (int) $pageIndex; if ($i < 0 || $i >= $n) { continue; } if (!is_string($base64Data) || trim($base64Data) === '') { continue; } if (!preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)/is', $base64Data, $m)) { continue; } $imageType = strtolower($m[1]); if ($imageType === 'jpeg') { $imageType = 'jpg'; } $b64 = preg_replace('/\s+/', '', $m[2]); $imageData = base64_decode($b64, true); if ($imageData === false || strlen($imageData) < 100) { continue; } $fn = 'page_' . $i . '_' . uniqid() . '_' . date('YmdHis') . '.' . $imageType; $full = $saveDir . $fn; if (!file_put_contents($full, $imageData)) { continue; } $dbPath = '/uploads/template/' . $dateYmd . '/' . $fn; $paths[$i] = $dbPath; $oss[$i] = Common::uploadLocalFileToOss((string) $full, (string) $dbPath); } return ['paths' => $paths, 'oss' => $oss]; } /** * 从 layers 解析最终 material_id 列表(与写入 relation 时逻辑一致,同一 id 可出现多次) */ protected function collectMaterialIdsFromLayers($layers, $layerIdToMaterial) { $ids = []; if (empty($layers) || !is_array($layers)) { return $ids; } foreach ($layers as $layer) { $materialId = $layer['material_id'] ?? null; if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) { $materialId = $layerIdToMaterial[$layer['id']]['id']; } if ($materialId !== null && $materialId !== '') { $ids[] = (int) $materialId; } } return $ids; } /** * 按图层引用次数增加 template_material.count */ protected function incrementMaterialUseCountsFromIds(array $materialIds) { if (empty($materialIds)) { return; } $counts = array_count_values($materialIds); foreach ($counts as $mid => $cnt) { if ($mid > 0 && $cnt > 0) { Db::name('template_material')->where('id', $mid)->setInc('count', $cnt); } } } /** * 某模版旧关联中各 material_id 出现几次,count 减几次(删除关联前调用) */ protected function decrementMaterialUseCountsByTemplateId($templateId) { $rows = Db::name('template_material_relation')->where('template_id', $templateId)->column('material_id'); if (empty($rows)) { return; } $ids = []; foreach ($rows as $mid) { if ($mid !== null && $mid !== '' && (int) $mid > 0) { $ids[] = (int) $mid; } } if (empty($ids)) { return; } $counts = array_count_values($ids); foreach ($counts as $mid => $cnt) { Db::name('template_material')->where('id', $mid)->setDec('count', $cnt); } } /** * 新增素材图片上传 * * 方式一(推荐,与当前前端一致):multipart/form-data * - Category_id:分类 ID * - img[] 或 img:多张图片文件(字段名 img,多文件用 img[]) * - material_name[] 或 material_name:与图片一一对应的名称数组 * - sys_id:可选 * * 方式二:JSON / 表单里传 uploaded_materials(base64) * - uploaded_materials=[{data:'data:image/png;base64,...', material_name:'xxx'}, ...] */ public function Material_Add() { $params = $this->request->param(); $sysId = trim($params['sys_id'] ?? ''); $categoryId = isset($params['Category_id']) ? intval($params['Category_id']) : null; $dateDir = date('Y-m-d'); $materialSavePath = str_replace('\\', '/', ROOT_PATH) . 'public/uploads/material/' . $dateDir . '/'; if (!is_dir($materialSavePath)) { mkdir($materialSavePath, 0755, true); } $uploaded = []; // 兼容单图(img)与多图(img[]),ThinkPHP 会返回对象或数组 $files = $this->request->file('img'); if (!empty($files)) { $fileList = is_array($files) ? $files : [$files]; $names = $params['material_name'] ?? []; if (!is_array($names)) { $names = [$names]; } foreach ($fileList as $i => $file) { if (!$file || !$file->isValid()) { continue; } // 先落本地(后续可用于备份/排障),再尝试同步 OSS $saveFileName = uniqid() . '_' . date('YmdHis') . '.' . $this->resolveUploadedImageExt($file); $info = $file->move($materialSavePath, $saveFileName); if (!$info) { continue; } $savedName = $info->getFilename(); $fullLocalPath = $materialSavePath . $savedName; $materialUrl = 'uploads/material/' . $dateDir . '/' . $savedName; // OSS 失败不阻断主流程(本地已保存) Common::uploadLocalFileToOss((string)$fullLocalPath, (string)$materialUrl); $materialRecord = [ 'sys_id' => $sysId, 'Category_id' => $categoryId, 'material_name' => trim($names[$i] ?? ''), 'material_url' => $materialUrl, 'create_time' => date('Y-m-d H:i:s'), 'count' => 1 ]; $materialId = Db::name('template_material')->insertGetId($materialRecord); $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl]; } } else { // 兼容旧版 base64 传参:uploaded_materials=[{data, material_name}, ...] $materials = $params['uploaded_materials'] ?? []; if (empty($materials) || !is_array($materials)) { return json(['code' => 1, 'msg' => '请上传至少一张素材图片(img/img[] 或 uploaded_materials)']); } foreach ($materials as $item) { $base64Data = $item['data'] ?? ''; if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\)]+)/i', $base64Data, $m)) { continue; } $imageType = strtolower($m[1]); $rawBase64 = preg_replace('/\s+/', '', $m[2]); $imageData = base64_decode($rawBase64); if ($imageData === false || strlen($imageData) < 100) { continue; } $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType; $fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext; $fullPath = $materialSavePath . $fileName; if (!file_put_contents($fullPath, $imageData)) { continue; } $materialUrl = 'uploads/material/' . $dateDir . '/' . $fileName; // base64 分支同样尝试同步 OSS Common::uploadLocalFileToOss((string)$fullPath, (string)$materialUrl); $materialRecord = [ 'sys_id' => $sysId, 'Category_id' => $categoryId, 'material_name' => trim($item['material_name'] ?? ''), 'material_url' => $materialUrl, 'create_time' => date('Y-m-d H:i:s'), 'count' => 1 ]; $materialId = Db::name('template_material')->insertGetId($materialRecord); $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl]; } } if (empty($uploaded)) { return json(['code' => 1, 'msg' => '没有有效的图片上传成功']); } return json(['code' => 0, 'msg' => '上传成功', 'data' => ['list' => $uploaded, 'count' => count($uploaded)]]); } /** * 素材图片删除:物理删库 + 删除 public 下对应图片文件 */ public function materialDelete() { $params = $this->request->param(); if (empty($params['id'])) { return json(['code' => 1, 'msg' => 'id 不能为空', 'data' => '']); } $id = intval($params['id']); $row = Db::name('template_material')->where('id', $id)->find(); if (!$row) { return json(['code' => 1, 'msg' => '记录不存在', 'data' => '']); } $list = Db::name('template_material_relation')->where('material_id', $id)->select(); if ($list) { return json([ 'code' => 1, 'msg' => '当前素材已被模版使用,不可删除', 'data' => '' ]); } $this->unlinkMaterialFileByUrl($row['material_url'] ?? ''); // 同步删除 OSS 对象(如未配置 OSS 或删除失败,不阻断数据库删除) Common::deleteOssObject((string)($row['material_url'] ?? '')); $res = Db::name('template_material')->where('id', $id)->delete(); if (!$res) { return json(['code' => 1, 'msg' => '删除失败', 'data' => '']); } return json(['code' => 0, 'msg' => '删除成功']); } /** * 素材修改 * 参数:id(必填), material_name(可选), Category_id(可选) */ public function Material_Update() { $params = $this->request->param(); if (empty($params['id']) || !is_numeric($params['id'])) { return json(['code' => 1, 'msg' => 'id 不能为空']); } $id = intval($params['id']); $row = Db::name('template_material')->where('id', $id)->find(); if (!$row) { return json(['code' => 1, 'msg' => '记录不存在']); } $update = ['update_time' => date('Y-m-d H:i:s')]; if (array_key_exists('material_name', $params)) { $update['material_name'] = trim($params['material_name'] ?? ''); } if (array_key_exists('Category_id', $params)) { $update['Category_id'] = $params['Category_id'] === '' || $params['Category_id'] === null ? null : intval($params['Category_id']); } if (count($update) <= 1) { return json(['code' => 1, 'msg' => '无有效修改字段']); } $affected = Db::name('template_material')->where('id', $id)->update($update); if ($affected > 0) { return json(['code' => 0, 'msg' => '修改成功']); } return json(['code' => 1, 'msg' => '修改失败']); } /** * 获取素材库列表接口(分页+搜索) * 参数:page(页码,从1开始), pageSize(每页条数,默认100,建议上限500) * 搜索:对 material_name、category_name 模糊匹配 */ public function Material_List(){ $params = $this->request->param(); $page = max(1, intval($params['page'] ?? 1)); $pageSize = min(500, max(1, intval($params['pageSize'] ?? 30))); $where = []; if (!empty($params['search'])) { // 使用更安全的查询方式,material_name 与 category_name 任一匹配即可 $search = trim($params['search']); $where['a.material_name|b.category_name|a.material_url'] = ['like', '%' . $search . '%']; } if (!empty($params['Category_id'])) { $where['a.Category_id'] = ['like', '%' . $params['Category_id'] . '%']; } $query = Db::name('template_material')->alias('a') ->field('a.id,a.sys_id,a.Category_id,b.category_name,a.material_name,a.material_url,a.count') ->join('template_material_category b', 'a.Category_id = b.id AND b.mod_rq IS NULL', 'LEFT') ->where($where) ->whereNull('a.mod_rq') ->order('a.id desc'); $total = (clone $query)->count(); $data = $query->page($page, $pageSize)->select(); foreach ($data as &$item) { if (!empty($item['material_url'])) { $item['material_url'] = Common::ossFullUrl((string)$item['material_url']); } } unset($item); return json([ 'code' => 0, 'msg' => '', 'data' => $data, 'total' => $total, 'count' => count($data) ]); } /** * 模板关联素材查询 */ public function Template_Material_Relation(){ $params = $this->request->param(); $res = Db::name('template_material_relation')->alias('a') ->field('a.*,c.chinese_description,c.page_image_urls, b.material_url,c.canvasWidth,c.canvasHeight,c.size') ->join('template_material b', 'a.material_id = b.id', 'left') ->join('product_template c', 'a.template_id = c.id', 'left') ->where('a.template_id',$params['id'])->select(); foreach ($res as &$item) { if (!empty($item['material_url'])) { $item['material_url'] = Common::ossFullUrl((string)$item['material_url']); } if (array_key_exists('chinese_description', $item)) { $item['chinese_description'] = Common::decodeChineseDescriptionForApi($item['chinese_description']); } if (array_key_exists('page_image_urls', $item) && $item['page_image_urls'] !== null && $item['page_image_urls'] !== '') { $arr = json_decode((string) $item['page_image_urls'], true); if (is_array($arr)) { foreach ($arr as &$pu) { if ($pu !== null && $pu !== '') { $pu = Common::ossFullUrl((string) $pu); } } unset($pu); $item['page_image_urls'] = $arr; } } } unset($item); // 处理null值,转换为空字符串 if($res){ foreach($res as &$item){ foreach($item as $key => &$value){ if($value === null){ $value = ''; } } } return json([ 'code' => 0, 'msg' => '', 'data' => $res, 'count' => count($res) ]); }else{ return json([ 'code' => 1, 'msg' => '此模版作品暂无素材图', 'data' => '', 'count' => 0 ]); } } /** * 新增模版(生成模版) */ public function Template_Material_Add(){ $params = $this->request->param(); // echo "
";
// print_r($params);
// echo "";die;
// 处理 uploaded_materials:保存素材图片到 uploads/material/ 并写入 template_material 表
$layerIdToMaterial = []; // layer_id => ['id'=>material_id, 'url'=>material_url]
$ossSync = ['configured' => Common::isOssEnabled(), 'materials' => [], 'template_main' => null, 'template_thumb' => null];
if (!empty($params['uploaded_materials'])) {
$materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
if (!is_dir($materialSavePath)) {
mkdir($materialSavePath, 0755, true);
}
foreach ($params['uploaded_materials'] as $item) {
$base64Data = $item['data'] ?? '';
if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
continue;
}
$imageType = strtolower($m[1]);
$imageData = base64_decode($m[2]);
if ($imageData === false || strlen($imageData) < 100) {
continue;
}
$ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
$fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
$fullPath = $materialSavePath . $fileName;
if (!file_put_contents($fullPath, $imageData)) {
continue;
}
$materialUrl = 'uploads/material/' . date('Y-m-d') . '/' . $fileName;
// uploaded_materials 分支:素材图先落本地,再尝试同步到 OSS(失败不影响新增模版)
$ossSync['materials'][] = [
'objectKey' => $materialUrl,
'ok' => Common::uploadLocalFileToOss((string)$fullPath, (string)$materialUrl),
];
$materialRecord = [
'sys_id' => $params['sys_id'] ?? '',
'material_url' => $materialUrl,
'type' => $item['type'] ?? '',
'Category_id' => $item['Category_id'] ?? '',
'chinese_description' => $item['chinese_description'] ?? '',
'material_name' => $item['material_name'] ?? '',
'create_time' => date('Y-m-d H:i:s'),
'count' => 1
];
$materialId = Db::name('template_material')->insertGetId($materialRecord);
if ($materialId && isset($item['layer_id'])) {
$layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
}
}
}
$save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d') . '/';
// 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
$save_path = str_replace('\\', '/', $save_path);
// 自动创建文件夹(如果不存在)
if (!is_dir($save_path)) {
mkdir($save_path, 0755, true);
}
// 提取base64图片数据
$previewImage = $params['previewImage'];
// 匹配base64图片数据
preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
if (empty($matches)) {
return '未找到图片数据';
}
$image_type = $matches[1];
$base64_data = $matches[2];
// 解码base64数据
$image_data = base64_decode($base64_data);
if ($image_data === false) {
return '图片解码失败';
}
// 生成唯一文件名(包含正确的扩展名)
$file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
$full_file_path = $save_path . $file_name;
// 保存图片到文件系统
if (!file_put_contents($full_file_path, $image_data)) {
return '图片保存失败';
}
// 生成数据库存储路径(使用正斜杠格式)
$db_img_path = '/uploads/template/'. date('Y-m-d') .'/' . $file_name;
// 预览图(模板原图)同步 OSS,保持本地/云端路径一致(失败见返回 oss_sync 与 runtime/log)
$ossSync['template_main'] = Common::uploadLocalFileToOss((string)$full_file_path, (string)$db_img_path);
// 生成缩略图
$thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
$db_thumbnail_path = '/uploads/template/'.date('Y-m-d') .'/' . $thumbnail_path;
$fullThumbnailPath = $save_path . $thumbnail_path;
// 缩略图文件存在时再同步 OSS,避免空文件名导致无效上传
if (!empty($thumbnail_path) && is_file($fullThumbnailPath)) {
$ossSync['template_thumb'] = Common::uploadLocalFileToOss((string)$fullThumbnailPath, (string)$db_thumbnail_path);
} else {
$ossSync['template_thumb'] = false;
}
$dateYmd = date('Y-m-d');
$newGalleryPathsForRollback = []; // 多页预览路径;插入失败时用于回滚删除
// 多页预览图:preview_images 与页下标一致,落盘 + OSS,JSON 存入 page_image_urls(TEXT,需建表字段)
if (!empty($params['preview_images']) && is_array($params['preview_images'])) {
$gal = $this->savePreviewGalleryBase64List($params['preview_images'], $save_path, $dateYmd);
$newGalleryPathsForRollback = $gal['paths'];
$ossSync['page_images'] = $gal['oss'];
}
//新增到模版表(product_template)
$record['toexamine'] = '审核通过';
$record['sys_id'] = $params['sys_id'];
// 多页提示词:chinese_description 为数组时下标 0=第 1 页…,入库 JSON;缺省存 []
$record['chinese_description'] = Common::encodeChineseDescriptionForDb($params['chinese_description'] ?? []);
$record['template_name'] = isset($params['template_name']) ? (string) $params['template_name'] : '';
$record['canvasWidth'] = $params['canvasWidth'];
$record['canvasHeight'] = $params['canvasHeight'];
$record['size'] = $params['canvasRatio'];
$record['template_image_url'] = $db_img_path;//原图
$record['thumbnail_image'] = $db_thumbnail_path;//缩略图
if (!empty($params['preview_images']) && is_array($params['preview_images'])) {
$record['page_image_urls'] = json_encode($newGalleryPathsForRollback, JSON_UNESCAPED_UNICODE);
}
$record['sys_rq'] = date('Y-m-d');
$record['create_time'] = date('Y-m-d H:i:s');
// 插入模板记录并获取ID
$templateId = Db::name('product_template')->insertGetId($record);
if (!$templateId) {
// 如果数据库插入失败,删除已保存的图片
if (file_exists($full_file_path)) {
unlink($full_file_path);
}
$this->deleteStoredPageGalleryImages($newGalleryPathsForRollback);
return '数据库插入失败';
}
// 处理layers数据,插入到模版-素材表(template_material_relation)
if (!empty($params['layers'])) {
$layers = $params['layers'];
foreach ($layers as $layer) {
$materialId = $layer['material_id'] ?? null;
$materialUrl = isset($layer['url']) ? $layer['url'] : '';
if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
$materialId = $layerIdToMaterial[$layer['id']]['id'];
$materialUrl = $layerIdToMaterial[$layer['id']]['url'];
}
$relationData = [
'template_id' => $templateId,//模版ID
'sys_id' => $params['sys_id'],//用户名
'material_id' => $materialId,//素材ID
'z_index' => $layer['id'],//层级
'layer_name' => $layer['name'],//图层名称
'layer_type' => $layer['type'],//类型
'material_url' => $materialUrl,//素材图片
'position_x' => $layer['x'],
'position_y' => $layer['y'],
'width' => $layer['width'],
'height' => $layer['height'],
'rotation' => $layer['rotation'],//素材旋转角度
'opacity' => $layer['opacity'],//素材透明度
'visible' => $layer['visible'],//图层是否显示
'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
//文字部分参数
'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
//形状与线条
'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
'page_index' => $this->layerPageIndex($layer),// 多页:0=第 1 页画布
'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
'create_time' => date('Y-m-d H:i:s')
];
// 插入关联记录
Db::name('template_material_relation')->insert($relationData);
}
$this->incrementMaterialUseCountsFromIds($this->collectMaterialIdsFromLayers($layers, $layerIdToMaterial));
}
$pageUrlsOut = [];
foreach ($newGalleryPathsForRollback as $pu) {
$pageUrlsOut[] = ($pu === '' || $pu === null) ? '' : Common::ossFullUrl((string) $pu);
}
return json([
'code' => 0,
'msg' => '',
'data' => '',
'template_id' => $templateId,
'template_image_url' => $db_img_path,
'template_image' => $db_thumbnail_path,
'page_image_urls' => $pageUrlsOut,
// OSS 是否开启、各文件是否上传成功(任一为 false 时查 runtime/log 中 [OSS uploadLocalFileToOss])
'oss_sync' => $ossSync,
]);
}
/**
* 修改模版
*/
public function Template_Material_Update(){
$params = $this->request->param();
// echo "";
// print_r($params);
// echo "";die;
// 验证模板ID
if (empty($params['template_id'])) {
return json([
'code' => 1,
'msg' => '模板ID不能为空',
'data' => ''
]);
}
$templateId = $params['template_id'];
// 检查模板是否存在
$template = Db::name('product_template')->where('id', $templateId)->find();
if (!$template) {
return json([
'code' => 1,
'msg' => '模板不存在',
'data' => ''
]);
}
$oldGalleryPathsToDeleteAfterDbOk = [];
$newGalleryPathsRollbackOnFail = [];
// 处理 uploaded_materials:修改时会有新的素材图上传,保存到 uploads/material/ 并写入 template_material 表(参考新增模版)
$layerIdToMaterial = [];
if (!empty($params['uploaded_materials'])) {
$materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
if (!is_dir($materialSavePath)) {
mkdir($materialSavePath, 0755, true);
}
foreach ($params['uploaded_materials'] as $item) {
$base64Data = $item['data'] ?? '';
if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
continue;
}
$imageType = strtolower($m[1]);
$imageData = base64_decode($m[2]);
if ($imageData === false || strlen($imageData) < 100) {
continue;
}
$ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
$fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
$fullPath = $materialSavePath . $fileName;
if (!file_put_contents($fullPath, $imageData)) {
continue;
}
$materialUrl = 'uploads/material/' . date('Y-m-d') . '/' . $fileName;
// 修改模版时新增素材:先本地保存,再同步 OSS(失败不阻塞更新)
Common::uploadLocalFileToOss((string)$fullPath, (string)$materialUrl);
$materialRecord = [
'sys_id' => $params['sys_id'] ?? '',
'material_url' => $materialUrl,
'type' => $item['type'] ?? '',
'Category_id' => $item['Category_id'] ?? '',
'chinese_description' => $item['chinese_description'] ?? '',
'material_name' => $item['material_name'] ?? '',
'create_time' => date('Y-m-d H:i:s'),
'count' => 0
];
$materialId = Db::name('template_material')->insertGetId($materialRecord);
if ($materialId && isset($item['layer_id'])) {
$layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
}
}
}
// 处理图片更新
$db_img_path = $template['template_image_url'];
$db_thumbnail_path = $template['thumbnail_image'];
if (!empty($params['previewImage'])) {
$save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d') . '/';
// 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
$save_path = str_replace('\\', '/', $save_path);
// 自动创建文件夹(如果不存在)
if (!is_dir($save_path)) {
mkdir($save_path, 0755, true);
}
// 提取base64图片数据
$previewImage = $params['previewImage'];
// 匹配base64图片数据
preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
if (empty($matches)) {
return json([
'code' => 1,
'msg' => '未找到图片数据',
'data' => ''
]);
}
$image_type = $matches[1];
$base64_data = $matches[2];
// 解码base64数据
$image_data = base64_decode($base64_data);
if ($image_data === false) {
return json([
'code' => 1,
'msg' => '图片解码失败',
'data' => ''
]);
}
// 生成唯一文件名(包含正确的扩展名)
$file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
$full_file_path = $save_path . $file_name;
// 保存图片到文件系统
if (!file_put_contents($full_file_path, $image_data)) {
return json([
'code' => 1,
'msg' => '图片保存失败',
'data' => ''
]);
}
// 生成数据库存储路径(使用正斜杠格式)
$db_img_path = '/uploads/template/'. date('Y-m-d') .'/' . $file_name;
// 修改模版预览图:同步 OSS,便于前端统一使用云端地址
Common::uploadLocalFileToOss((string)$full_file_path, (string)$db_img_path);
// 生成缩略图
$thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
$db_thumbnail_path = '/uploads/template/'.date('Y-m-d') .'/' . $thumbnail_path;
$fullThumbnailPath = $save_path . $thumbnail_path;
// 修改模版缩略图:存在则同步 OSS
if (!empty($thumbnail_path) && is_file($fullThumbnailPath)) {
Common::uploadLocalFileToOss((string)$fullThumbnailPath, (string)$db_thumbnail_path);
}
// 删除旧图片
if (!empty($template['template_image_url'])) {
$oldImagePath = ROOT_PATH . 'public' . $template['template_image_url'];
if (file_exists($oldImagePath)) {
unlink($oldImagePath);
}
}
if (!empty($template['thumbnail_image'])) {
$oldThumbnailPath = ROOT_PATH . 'public' . $template['thumbnail_image'];
if (file_exists($oldThumbnailPath)) {
unlink($oldThumbnailPath);
}
}
}
// 更新模版表(product_template)
$record['canvasWidth'] = $params['canvasWidth'];
$record['canvasHeight'] = $params['canvasHeight'];
$record['size'] = $params['canvasRatio'];
if (!empty($db_img_path)) {
$record['template_image_url'] = $db_img_path;//原图
$record['thumbnail_image'] = $db_thumbnail_path;//缩略图
}
if (array_key_exists('preview_images', $params) && is_array($params['preview_images'])) {
$oldGalleryPathsToDeleteAfterDbOk = $this->decodePageImageUrlsField($template['page_image_urls'] ?? null);
$saveGal = str_replace('\\', '/', ROOT_PATH . 'public/uploads/template/' . date('Y-m-d') . '/');
if (!is_dir($saveGal)) {
mkdir($saveGal, 0755, true);
}
$gal = $this->savePreviewGalleryBase64List($params['preview_images'], $saveGal, date('Y-m-d'));
$newGalleryPathsRollbackOnFail = $gal['paths'];
$record['page_image_urls'] = json_encode($gal['paths'], JSON_UNESCAPED_UNICODE);
}
$record['sys_rq'] = date('Y-m-d');
if (array_key_exists('chinese_description', $params)) {
$record['chinese_description'] = Common::encodeChineseDescriptionForDb($params['chinese_description']);
}
if (array_key_exists('template_name', $params)) {
$record['template_name'] = (string) $params['template_name'];
}
$record['update_time'] = date('Y-m-d H:i:s');
// 更新模板记录
$res = Db::name('product_template')->where('id', $templateId)->update($record);
if (!$res) {
$this->deleteStoredPageGalleryImages($newGalleryPathsRollbackOnFail);
return json([
'code' => 1,
'msg' => '数据库更新失败',
'data' => ''
]);
}
if ($oldGalleryPathsToDeleteAfterDbOk !== []) {
foreach ($oldGalleryPathsToDeleteAfterDbOk as $p) {
if ($p === null || $p === '') {
continue;
}
$this->unlinkMaterialFileByUrl((string) $p);
Common::deleteOssObject((string) $p);
}
}
// 处理layers数据,更新模版-素材表(template_material_relation)
if (!empty($params['layers'])) {
$this->decrementMaterialUseCountsByTemplateId($templateId);
// 删除旧的关联记录
Db::name('template_material_relation')->where('template_id', $templateId)->delete();
$layers = $params['layers'];
foreach ($layers as $layer) {
$materialId = $layer['material_id'] ?? null;
$materialUrl = isset($layer['url']) ? $layer['url'] : '';
if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
$materialId = $layerIdToMaterial[$layer['id']]['id'];
$materialUrl = $layerIdToMaterial[$layer['id']]['url'];
}
$relationData = [
'template_id' => $templateId,//模版ID
'sys_id' => $params['sys_id'] ?? '',//用户名
'material_id' => $materialId,//素材ID
'z_index' => $layer['id'],//层级
'layer_name' => $layer['name'],//图层名称
'layer_type' => $layer['type'],//类型
'material_url' => $materialUrl,//素材图片
'position_x' => $layer['x'],
'position_y' => $layer['y'],
'width' => $layer['width'],
'height' => $layer['height'],
'rotation' => $layer['rotation'],//素材旋转角度
'opacity' => $layer['opacity'],//素材透明度
'visible' => $layer['visible'],//图层是否显示
'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
//文字部分参数
'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
//形状与线条
'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
'page_index' => $this->layerPageIndex($layer),// 多页:0=第 1 页画布
'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
'create_time' => date('Y-m-d H:i:s')
];
// 插入关联记录
Db::name('template_material_relation')->insert($relationData);
}
$this->incrementMaterialUseCountsFromIds($this->collectMaterialIdsFromLayers($layers, $layerIdToMaterial));
}
$pageGalleryOut = array_key_exists('preview_images', $params)
? $newGalleryPathsRollbackOnFail
: $this->decodePageImageUrlsField($template['page_image_urls'] ?? null);
foreach ($pageGalleryOut as &$gp) {
if ($gp !== null && $gp !== '') {
$gp = Common::ossFullUrl((string) $gp);
}
}
unset($gp);
return json([
'code' => 0,
'msg' => '修改成功',
'data' => '',
'template_id' => $templateId,
'template_image_url' => $db_img_path,
'template_image' => $db_thumbnail_path,
'page_image_urls' => $pageGalleryOut,
]);
}
/**
* 生成缩略图(高质量)
* @param string $originalPath 原图路径
* @param string $savePath 保存目录
* @param string $fileName 原文件名
* @return string 缩略图文件名
*/
private function generateThumbnail($originalPath, $savePath, $fileName) {
// 获取图片信息
$imageInfo = getimagesize($originalPath);
if (!$imageInfo) {
return '';
}
$width = $imageInfo[0];
$height = $imageInfo[1];
// 计算缩略图尺寸(保持比例,最大宽度400)
$maxWidth = 400;
$maxHeight = 400;
if ($width > $maxWidth || $height > $maxHeight) {
$ratio = min($maxWidth / $width, $maxHeight / $height);
$thumbWidth = round($width * $ratio);
$thumbHeight = round($height * $ratio);
} else {
$thumbWidth = $width;
$thumbHeight = $height;
}
// 创建缩略图画布
$thumbnail = imagecreatetruecolor($thumbWidth, $thumbHeight);
// 根据图片类型创建图像资源
switch ($imageInfo[2]) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($originalPath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($originalPath);
// 处理PNG透明
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
imagefilledrectangle($thumbnail, 0, 0, $thumbWidth, $thumbHeight, $transparent);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($originalPath);
break;
default:
return '';
}
if (!$source) {
return '';
}
// 调整图片大小(使用高质量缩放)
imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
// 生成缩略图文件名
$pathInfo = pathinfo($fileName);
$thumbnailName = $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
$thumbnailPath = $savePath . $thumbnailName;
// 保存缩略图(使用高质量设置)
switch ($imageInfo[2]) {
case IMAGETYPE_JPEG:
imagejpeg($thumbnail, $thumbnailPath, 95); // 95% 质量,接近原图
break;
case IMAGETYPE_PNG:
imagepng($thumbnail, $thumbnailPath, 3); // 压缩级别 3,保持较高质量
break;
case IMAGETYPE_GIF:
imagegif($thumbnail, $thumbnailPath);
break;
}
// 释放资源
imagedestroy($source);
imagedestroy($thumbnail);
return $thumbnailName;
}
/**
* 发布模版(release=1)
*/
public function Template_Material_Publish(){
$params = $this->request->param();
$record['release'] = 1;
$record['update_time'] = date('Y-m-d H:i:s');
$res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
if (!$res) {
return json([
'code' => 1,
'msg' => '发布失败',
'data' => ''
]);
}
return json([
'code' => 0,
'msg' => '发布成功'
]);
}
/**
* 取消发布模版(release=0)
*/
public function Template_Material_Unpublish(){
$params = $this->request->param();
$record['release'] = 0;
$record['update_time'] = date('Y-m-d H:i:s');
$res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
if (!$res) {
return json([
'code' => 1,
'msg' => '发布失败',
'data' => ''
]);
}
return json([
'code' => 0,
'msg' => '发布成功'
]);
}
/**
* 删除模版
*/
public function Template_Material_Delete(){
$params = $this->request->param();
if (empty($params['template_id']) || !is_numeric($params['template_id'])) {
return json([
'code' => 1,
'msg' => 'template_id 参数错误',
'data' => ''
]);
}
$templateId = intval($params['template_id']);
$template = Db::name('product_template')->where('id', $templateId)->find();
if (!$template) {
return json([
'code' => 1,
'msg' => '模版不存在',
'data' => ''
]);
}
// 删除本地文件
$this->unlinkMaterialFileByUrl($template['template_image_url'] ?? '');
$this->unlinkMaterialFileByUrl($template['thumbnail_image'] ?? '');
$this->deleteStoredPageGalleryImages($template['page_image_urls'] ?? '');
// 删除 OSS 文件(失败不阻断主流程)
Common::deleteOssObject((string)($template['template_image_url'] ?? ''));
Common::deleteOssObject((string)($template['thumbnail_image'] ?? ''));
// 删除模板记录(物理删除)
$res = Db::name('product_template')->where('id', $templateId)->delete();
if (!$res) {
return json([
'code' => 1,
'msg' => '模版删除失败',
'data' => ''
]);
}
return json([
'code' => 0,
'msg' => '模版删除成功'
]);
}
/**
* 素材分类查询(一级+二级树形结构,供前端展示)
*/
public function Material_Category_List()
{
$all = Db::name('template_material_category')
->field('id,category_name,parent_id,sort')
->where('status', 1)
->whereNull('mod_rq')
->order('sort', 'asc')
->select();
// 分离一级(parent_id=0)和二级
$level1 = [];
$level2ByParent = [];
foreach ($all as $row) {
if ($row['parent_id'] == 0) {
$row['children'] = [];
$level1[] = $row;
} else {
$pid = $row['parent_id'];
if (!isset($level2ByParent[$pid])) {
$level2ByParent[$pid] = [];
}
$level2ByParent[$pid][] = $row;
}
}
// 按 sort 排序一级,并把二级挂到对应一级下
usort($level1, function ($a, $b) {
return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
});
foreach ($level1 as &$item) {
$item['children'] = $level2ByParent[$item['id']] ?? [];
usort($item['children'], function ($a, $b) {
return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
});
}
return json(['code' => 0, 'msg' => '', 'data' => $level1]);
}
/**
* 新增素材分类(支持一级、二级)
* 一级:parent_id 不传或传 0
* 二级:parent_id 传一级分类的 id
* 参数:category_name(必填), parent_id(可选,默认0), sort(可选,默认0), status(可选,默认1)
*/
public function Material_Category_Add()
{
$params = $this->request->param();
if (empty(trim($params['category_name'] ?? ''))) {
return json(['code' => 1, 'msg' => '分类名称不能为空']);
}
$parentId = isset($params['parent_id']) ? intval($params['parent_id']) : 0;
// 二级分类:校验父分类存在且未软删除
if ($parentId > 0) {
$parent = Db::name('template_material_category')
->where('id', $parentId)
->where('parent_id', 0)
->whereNull('mod_rq')
->find();
if (!$parent) {
return json(['code' => 1, 'msg' => '父分类不存在或已删除']);
}
}
$data = [
'category_name' => trim($params['category_name']),
'parent_id' => $parentId,
'sort' => $params['sort'] ?? 0,
'status' => $params['status'] ?? '1',
'createtime' => date('Y-m-d H:i:s')
];
$id = Db::name('template_material_category')->insertGetId($data);
if ($id) {
return json(['code' => 0, 'msg' => '新增成功', 'data' => ['id' => $id]]);
}
return json(['code' => 1, 'msg' => '新增失败']);
}
/**
* 修改素材分类
* 参数:id(必填), category_name/sort/status/parent_id(可选)
*/
public function Material_Category_Update()
{
$params = $this->request->param();
if (empty($params['id']) || !is_numeric($params['id'])) {
return json(['code' => 1, 'msg' => '参数错误']);
}
$data = ['updatetime' => date('Y-m-d H:i:s')];
if (isset($params['category_name']) && trim($params['category_name']) !== '') {
$data['category_name'] = trim($params['category_name']);
}
if (isset($params['parent_id'])) {
$data['parent_id'] = intval($params['parent_id']);
}
if (isset($params['sort'])) {
$data['sort'] = $params['sort'];
}
if (isset($params['status'])) {
$data['status'] = $params['status'];
}
$affected = Db::name('template_material_category')
->where('id', intval($params['id']))
->whereNull('mod_rq')
->update($data);
if ($affected > 0) {
return json(['code' => 0, 'msg' => '修改成功']);
}
return json(['code' => 1, 'msg' => '记录不存在或已删除']);
}
/**
* 删除素材分类(软删除,设置 mod_rq)
*/
public function Material_Category_Delete()
{
$params = $this->request->param();
if (empty($params['id']) || !is_numeric($params['id'])) {
return json(['code' => 1, 'msg' => '参数错误']);
}
$affected = Db::name('template_material_category')
->where('id', intval($params['id']))
->update(['mod_rq' => date('Y-m-d H:i:s')]);
if ($affected > 0) {
return json(['code' => 0, 'msg' => '删除成功']);
}
return json(['code' => 1, 'msg' => '记录不存在或已删除']);
}
}