Material.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\service\AIGatewayService;
  5. use think\Db;
  6. use think\Exception;
  7. class Material extends Api
  8. {
  9. protected $noNeedLogin = ['*'];
  10. protected $noNeedRight = ['*'];
  11. public function index()
  12. {
  13. $this->success('Material');
  14. }
  15. /**
  16. * 上传图片保存扩展名:优先客户端文件名,无后缀时用 MIME 推断,避免生成 xxx. 无扩展名
  17. */
  18. protected function resolveUploadedImageExt($file)
  19. {
  20. $name = $file->getInfo('name') ?? '';
  21. $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  22. if ($ext === 'jpeg') {
  23. $ext = 'jpg';
  24. }
  25. $allowed = ['jpg', 'png', 'gif', 'webp', 'bmp'];
  26. if ($ext && in_array($ext, $allowed, true)) {
  27. return $ext;
  28. }
  29. $mime = method_exists($file, 'getMime') ? strtolower((string) $file->getMime()) : '';
  30. $map = [
  31. 'image/jpeg' => 'jpg',
  32. 'image/png' => 'png',
  33. 'image/gif' => 'gif',
  34. 'image/webp' => 'webp',
  35. 'image/bmp' => 'bmp',
  36. ];
  37. if (isset($map[$mime])) {
  38. return $map[$mime];
  39. }
  40. return 'jpg';
  41. }
  42. /**
  43. * 新增素材图片上传
  44. *
  45. * 方式一(推荐,与当前前端一致):multipart/form-data
  46. * - Category_id:分类 ID
  47. * - img[] 或 img:多张图片文件(字段名 img,多文件用 img[])
  48. * - material_name[] 或 material_name:与图片一一对应的名称数组
  49. * - sys_id:可选
  50. *
  51. * 方式二:JSON / 表单里传 uploaded_materials(base64)
  52. * - uploaded_materials=[{data:'data:image/png;base64,...', material_name:'xxx'}, ...]
  53. */
  54. public function Material_Add()
  55. {
  56. $params = $this->request->param();
  57. $sysId = trim($params['sys_id'] ?? '');
  58. $categoryId = isset($params['Category_id']) ? intval($params['Category_id']) : null;
  59. $dateDir = date('Y-m-d');
  60. $materialSavePath = str_replace('\\', '/', ROOT_PATH) . 'public/uploads/material/' . $dateDir . '/';
  61. if (!is_dir($materialSavePath)) {
  62. mkdir($materialSavePath, 0755, true);
  63. }
  64. $uploaded = [];
  65. $files = $this->request->file('img');
  66. if (!empty($files)) {
  67. $fileList = is_array($files) ? $files : [$files];
  68. $names = $params['material_name'] ?? [];
  69. if (!is_array($names)) {
  70. $names = [$names];
  71. }
  72. foreach ($fileList as $i => $file) {
  73. if (!$file || !$file->isValid()) {
  74. continue;
  75. }
  76. $saveFileName = uniqid() . '_' . date('YmdHis') . '.' . $this->resolveUploadedImageExt($file);
  77. $info = $file->move($materialSavePath, $saveFileName);
  78. if (!$info) {
  79. continue;
  80. }
  81. $savedName = $info->getFilename();
  82. $materialUrl = '/uploads/material/' . $dateDir . '/' . $savedName;
  83. $materialRecord = [
  84. 'sys_id' => $sysId,
  85. 'Category_id' => $categoryId,
  86. 'material_name' => trim($names[$i] ?? ''),
  87. 'material_url' => $materialUrl,
  88. 'create_time' => date('Y-m-d H:i:s'),
  89. 'count' => 1
  90. ];
  91. $materialId = Db::name('template_material')->insertGetId($materialRecord);
  92. $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl];
  93. }
  94. } else {
  95. $materials = $params['uploaded_materials'] ?? [];
  96. if (empty($materials) || !is_array($materials)) {
  97. return json(['code' => 1, 'msg' => '请上传至少一张素材图片(img/img[] 或 uploaded_materials)']);
  98. }
  99. foreach ($materials as $item) {
  100. $base64Data = $item['data'] ?? '';
  101. if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\)]+)/i', $base64Data, $m)) {
  102. continue;
  103. }
  104. $imageType = strtolower($m[1]);
  105. $rawBase64 = preg_replace('/\s+/', '', $m[2]);
  106. $imageData = base64_decode($rawBase64);
  107. if ($imageData === false || strlen($imageData) < 100) {
  108. continue;
  109. }
  110. $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
  111. $fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
  112. $fullPath = $materialSavePath . $fileName;
  113. if (!file_put_contents($fullPath, $imageData)) {
  114. continue;
  115. }
  116. $materialUrl = '/uploads/material/' . $dateDir . '/' . $fileName;
  117. $materialRecord = [
  118. 'sys_id' => $sysId,
  119. 'Category_id' => $categoryId,
  120. 'material_name' => trim($item['material_name'] ?? ''),
  121. 'material_url' => $materialUrl,
  122. 'create_time' => date('Y-m-d H:i:s'),
  123. 'count' => 1
  124. ];
  125. $materialId = Db::name('template_material')->insertGetId($materialRecord);
  126. $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl];
  127. }
  128. }
  129. if (empty($uploaded)) {
  130. return json(['code' => 1, 'msg' => '没有有效的图片上传成功']);
  131. }
  132. return json(['code' => 0, 'msg' => '上传成功', 'data' => ['list' => $uploaded, 'count' => count($uploaded)]]);
  133. }
  134. /**
  135. * 素材图片删除(软删除)
  136. */
  137. public function materialDelete()
  138. {
  139. $params = $this->request->param();
  140. $record['mod_rq'] = date('Y-m-d H:i:s');
  141. $res = Db::name('template_material')->where('id', $params['id'])->update($record);
  142. if (!$res) {
  143. return json([
  144. 'code' => 1,
  145. 'msg' => '删除失败',
  146. 'data' => ''
  147. ]);
  148. }
  149. return json([
  150. 'code' => 0,
  151. 'msg' => '删除成功'
  152. ]);
  153. }
  154. /**
  155. * 素材修改
  156. * 参数:id(必填), material_name(可选), Category_id(可选)
  157. */
  158. public function Material_Update()
  159. {
  160. $params = $this->request->param();
  161. if (empty($params['id']) || !is_numeric($params['id'])) {
  162. return json(['code' => 1, 'msg' => 'id 不能为空']);
  163. }
  164. $id = intval($params['id']);
  165. $row = Db::name('template_material')->where('id', $id)->whereNull('mod_rq')->find();
  166. if (!$row) {
  167. return json(['code' => 1, 'msg' => '记录不存在或已删除']);
  168. }
  169. $update = ['update_time' => date('Y-m-d H:i:s')];
  170. if (array_key_exists('material_name', $params)) {
  171. $update['material_name'] = trim($params['material_name'] ?? '');
  172. }
  173. if (array_key_exists('Category_id', $params)) {
  174. $update['Category_id'] = $params['Category_id'] === '' || $params['Category_id'] === null ? null : intval($params['Category_id']);
  175. }
  176. if (count($update) <= 1) {
  177. return json(['code' => 1, 'msg' => '无有效修改字段']);
  178. }
  179. $affected = Db::name('template_material')->where('id', $id)->update($update);
  180. if ($affected > 0) {
  181. return json(['code' => 0, 'msg' => '修改成功']);
  182. }
  183. return json(['code' => 1, 'msg' => '修改失败']);
  184. }
  185. /**
  186. * 获取素材库列表接口(分页+搜索)
  187. * 参数:page(页码,从1开始), pageSize(每页条数,默认100,建议上限500)
  188. * 搜索:对 material_name、category_name 模糊匹配
  189. */
  190. public function Material_List(){
  191. $params = $this->request->param();
  192. $page = max(1, intval($params['page'] ?? 1));
  193. $pageSize = min(500, max(1, intval($params['pageSize'] ?? 100)));
  194. $where = [];
  195. if (!empty($params['search'])) {
  196. // 使用更安全的查询方式,material_name 与 category_name 任一匹配即可
  197. $search = trim($params['search']);
  198. $where['a.material_name|b.category_name'] = ['like', '%' . $search . '%'];
  199. }
  200. if (!empty($params['Category_id'])) {
  201. $where['a.Category_id'] = ['like', '%' . $params['Category_id'] . '%'];
  202. }
  203. $query = Db::name('template_material')->alias('a')
  204. ->field('a.id,a.sys_id,a.Category_id,b.category_name,a.material_name,a.material_url')
  205. ->join('template_material_category b', 'a.Category_id = b.id AND b.mod_rq IS NULL', 'LEFT')
  206. ->where($where)
  207. ->whereNull('a.mod_rq')
  208. ->order('a.id desc');
  209. $total = (clone $query)->count();
  210. $data = $query->page($page, $pageSize)->select();
  211. return json([
  212. 'code' => 0,
  213. 'msg' => '',
  214. 'data' => $data,
  215. 'total' => $total,
  216. 'count' => count($data)
  217. ]);
  218. }
  219. /**
  220. * 模板关联素材查询
  221. */
  222. public function Template_Material_Relation(){
  223. $params = $this->request->param();
  224. $res = Db::name('template_material_relation')->alias('a')
  225. ->field('a.*, b.material_url,c.canvasWidth,c.canvasHeight,c.size')
  226. ->join('template_material b', 'a.material_id = b.id', 'left')
  227. ->join('product_template c', 'a.template_id = c.id', 'left')
  228. ->where('a.template_id',$params['id'])->select();
  229. // 处理null值,转换为空字符串
  230. if($res){
  231. foreach($res as &$item){
  232. foreach($item as $key => &$value){
  233. if($value === null){
  234. $value = '';
  235. }
  236. }
  237. }
  238. return json([
  239. 'code' => 0,
  240. 'msg' => '',
  241. 'data' => $res,
  242. 'count' => count($res)
  243. ]);
  244. }else{
  245. return json([
  246. 'code' => 1,
  247. 'msg' => '此模版作品暂无素材图',
  248. 'data' => '',
  249. 'count' => 0
  250. ]);
  251. }
  252. }
  253. /**
  254. * 新增模版(生成模版)
  255. */
  256. public function Template_Material_Add(){
  257. $params = $this->request->param();
  258. // echo "<pre>";
  259. // print_r($params);
  260. // echo "<pre>";die;
  261. // 处理 uploaded_materials:保存素材图片到 uploads/material/ 并写入 template_material 表
  262. $layerIdToMaterial = []; // layer_id => ['id'=>material_id, 'url'=>material_url]
  263. if (!empty($params['uploaded_materials'])) {
  264. $materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
  265. if (!is_dir($materialSavePath)) {
  266. mkdir($materialSavePath, 0755, true);
  267. }
  268. foreach ($params['uploaded_materials'] as $item) {
  269. $base64Data = $item['data'] ?? '';
  270. if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
  271. continue;
  272. }
  273. $imageType = strtolower($m[1]);
  274. $imageData = base64_decode($m[2]);
  275. if ($imageData === false || strlen($imageData) < 100) {
  276. continue;
  277. }
  278. $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
  279. $fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
  280. $fullPath = $materialSavePath . $fileName;
  281. if (!file_put_contents($fullPath, $imageData)) {
  282. continue;
  283. }
  284. $materialUrl = 'uploads/material/' . date('Y-m-d') . '/' . $fileName;
  285. $materialRecord = [
  286. 'sys_id' => $params['sys_id'] ?? '',
  287. 'material_url' => $materialUrl,
  288. 'type' => $item['type'] ?? '',
  289. 'create_time' => date('Y-m-d H:i:s'),
  290. 'count' => 1
  291. ];
  292. $materialId = Db::name('template_material')->insertGetId($materialRecord);
  293. if ($materialId && isset($item['layer_id'])) {
  294. $layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
  295. }
  296. }
  297. }
  298. $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d') . '/';
  299. // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
  300. $save_path = str_replace('\\', '/', $save_path);
  301. // 自动创建文件夹(如果不存在)
  302. if (!is_dir($save_path)) {
  303. mkdir($save_path, 0755, true);
  304. }
  305. // 提取base64图片数据
  306. $previewImage = $params['previewImage'];
  307. // 匹配base64图片数据
  308. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
  309. if (empty($matches)) {
  310. return '未找到图片数据';
  311. }
  312. $image_type = $matches[1];
  313. $base64_data = $matches[2];
  314. // 解码base64数据
  315. $image_data = base64_decode($base64_data);
  316. if ($image_data === false) {
  317. return '图片解码失败';
  318. }
  319. // 生成唯一文件名(包含正确的扩展名)
  320. $file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
  321. $full_file_path = $save_path . $file_name;
  322. // 保存图片到文件系统
  323. if (!file_put_contents($full_file_path, $image_data)) {
  324. return '图片保存失败';
  325. }
  326. // 生成数据库存储路径(使用正斜杠格式)
  327. $db_img_path = '/uploads/template/'. date('Y-m-d') .'/' . $file_name;
  328. // 生成缩略图
  329. $thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
  330. $db_thumbnail_path = '/uploads/template/'.date('Y-m-d') .'/' . $thumbnail_path;
  331. //新增到模版表(product_template)
  332. $record['toexamine'] = '审核通过';
  333. $record['sys_id'] = $params['sys_id'];
  334. $record['canvasWidth'] = $params['canvasWidth'];
  335. $record['canvasHeight'] = $params['canvasHeight'];
  336. $record['size'] = $params['canvasRatio'];
  337. $record['template_image_url'] = $db_img_path;//原图
  338. $record['thumbnail_image'] = $db_thumbnail_path;//缩略图
  339. $record['sys_rq'] = date('Y-m-d');
  340. $record['create_time'] = date('Y-m-d H:i:s');
  341. // 插入模板记录并获取ID
  342. $templateId = Db::name('product_template')->insertGetId($record);
  343. if (!$templateId) {
  344. // 如果数据库插入失败,删除已保存的图片
  345. if (file_exists($full_file_path)) {
  346. unlink($full_file_path);
  347. }
  348. return '数据库插入失败';
  349. }
  350. // 处理layers数据,插入到模版-素材表(template_material_relation)
  351. if (!empty($params['layers'])) {
  352. $layers = $params['layers'];
  353. foreach ($layers as $layer) {
  354. $materialId = $layer['material_id'] ?? null;
  355. $materialUrl = isset($layer['url']) ? $layer['url'] : '';
  356. if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
  357. $materialId = $layerIdToMaterial[$layer['id']]['id'];
  358. $materialUrl = $layerIdToMaterial[$layer['id']]['url'];
  359. }
  360. $relationData = [
  361. 'template_id' => $templateId,//模版ID
  362. 'sys_id' => $params['sys_id'],//用户名
  363. 'material_id' => $materialId,//素材ID
  364. 'z_index' => $layer['id'],//层级
  365. 'layer_name' => $layer['name'],//图层名称
  366. 'layer_type' => $layer['type'],//类型
  367. 'material_url' => $materialUrl,//素材图片
  368. 'position_x' => $layer['x'],
  369. 'position_y' => $layer['y'],
  370. 'width' => $layer['width'],
  371. 'height' => $layer['height'],
  372. 'rotation' => $layer['rotation'],//素材旋转角度
  373. 'opacity' => $layer['opacity'],//素材透明度
  374. 'visible' => $layer['visible'],//图层是否显示
  375. 'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
  376. //文字部分参数
  377. 'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
  378. 'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
  379. 'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
  380. 'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
  381. 'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
  382. 'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
  383. 'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
  384. 'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
  385. 'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
  386. 'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
  387. 'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
  388. 'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
  389. //形状与线条
  390. 'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
  391. 'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
  392. 'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
  393. 'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
  394. 'page_index' => $layer['page_index'] ?? $layer['page_index'] ?? '',//画布分页排序
  395. 'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
  396. 'create_time' => date('Y-m-d H:i:s')
  397. ];
  398. // 插入关联记录
  399. Db::name('template_material_relation')->insert($relationData);
  400. }
  401. }
  402. return json([
  403. 'code' => 0,
  404. 'msg' => '',
  405. 'data' => '',
  406. 'template_id' => $templateId,
  407. 'template_image_url' => $db_img_path,
  408. 'template_image' => $db_thumbnail_path
  409. ]);
  410. }
  411. /**
  412. * 修改模版
  413. */
  414. public function Template_Material_Update(){
  415. $params = $this->request->param();
  416. // echo "<pre>";
  417. // print_r($params);
  418. // echo "<pre>";die;
  419. // 验证模板ID
  420. if (empty($params['template_id'])) {
  421. return json([
  422. 'code' => 1,
  423. 'msg' => '模板ID不能为空',
  424. 'data' => ''
  425. ]);
  426. }
  427. $templateId = $params['template_id'];
  428. // 检查模板是否存在
  429. $template = Db::name('product_template')->where('id', $templateId)->find();
  430. if (!$template) {
  431. return json([
  432. 'code' => 1,
  433. 'msg' => '模板不存在',
  434. 'data' => ''
  435. ]);
  436. }
  437. // 处理 uploaded_materials:修改时会有新的素材图上传,保存到 uploads/material/ 并写入 template_material 表(参考新增模版)
  438. $layerIdToMaterial = [];
  439. if (!empty($params['uploaded_materials'])) {
  440. $materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
  441. if (!is_dir($materialSavePath)) {
  442. mkdir($materialSavePath, 0755, true);
  443. }
  444. foreach ($params['uploaded_materials'] as $item) {
  445. $base64Data = $item['data'] ?? '';
  446. if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
  447. continue;
  448. }
  449. $imageType = strtolower($m[1]);
  450. $imageData = base64_decode($m[2]);
  451. if ($imageData === false || strlen($imageData) < 100) {
  452. continue;
  453. }
  454. $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
  455. $fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
  456. $fullPath = $materialSavePath . $fileName;
  457. if (!file_put_contents($fullPath, $imageData)) {
  458. continue;
  459. }
  460. $materialUrl = 'uploads/material/' . date('Y-m-d') . '/' . $fileName;
  461. $materialRecord = [
  462. 'sys_id' => $params['sys_id'] ?? '',
  463. 'material_url' => $materialUrl,
  464. 'type' => $item['type'] ?? '',
  465. 'create_time' => date('Y-m-d H:i:s'),
  466. 'count' => 1
  467. ];
  468. $materialId = Db::name('template_material')->insertGetId($materialRecord);
  469. if ($materialId && isset($item['layer_id'])) {
  470. $layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
  471. }
  472. }
  473. }
  474. // 处理图片更新
  475. $db_img_path = $template['template_image_url'];
  476. $db_thumbnail_path = $template['thumbnail_image'];
  477. if (!empty($params['previewImage'])) {
  478. $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d') . '/';
  479. // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
  480. $save_path = str_replace('\\', '/', $save_path);
  481. // 自动创建文件夹(如果不存在)
  482. if (!is_dir($save_path)) {
  483. mkdir($save_path, 0755, true);
  484. }
  485. // 提取base64图片数据
  486. $previewImage = $params['previewImage'];
  487. // 匹配base64图片数据
  488. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
  489. if (empty($matches)) {
  490. return json([
  491. 'code' => 1,
  492. 'msg' => '未找到图片数据',
  493. 'data' => ''
  494. ]);
  495. }
  496. $image_type = $matches[1];
  497. $base64_data = $matches[2];
  498. // 解码base64数据
  499. $image_data = base64_decode($base64_data);
  500. if ($image_data === false) {
  501. return json([
  502. 'code' => 1,
  503. 'msg' => '图片解码失败',
  504. 'data' => ''
  505. ]);
  506. }
  507. // 生成唯一文件名(包含正确的扩展名)
  508. $file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
  509. $full_file_path = $save_path . $file_name;
  510. // 保存图片到文件系统
  511. if (!file_put_contents($full_file_path, $image_data)) {
  512. return json([
  513. 'code' => 1,
  514. 'msg' => '图片保存失败',
  515. 'data' => ''
  516. ]);
  517. }
  518. // 生成数据库存储路径(使用正斜杠格式)
  519. $db_img_path = '/uploads/template/'. date('Y-m-d') .'/' . $file_name;
  520. // 生成缩略图
  521. $thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
  522. $db_thumbnail_path = '/uploads/template/'.date('Y-m-d') .'/' . $thumbnail_path;
  523. // 删除旧图片
  524. if (!empty($template['template_image_url'])) {
  525. $oldImagePath = ROOT_PATH . 'public' . $template['template_image_url'];
  526. if (file_exists($oldImagePath)) {
  527. unlink($oldImagePath);
  528. }
  529. }
  530. if (!empty($template['thumbnail_image'])) {
  531. $oldThumbnailPath = ROOT_PATH . 'public' . $template['thumbnail_image'];
  532. if (file_exists($oldThumbnailPath)) {
  533. unlink($oldThumbnailPath);
  534. }
  535. }
  536. }
  537. // 更新模版表(product_template)
  538. $record['canvasWidth'] = $params['canvasWidth'];
  539. $record['canvasHeight'] = $params['canvasHeight'];
  540. $record['size'] = $params['canvasRatio'];
  541. if (!empty($db_img_path)) {
  542. $record['template_image_url'] = $db_img_path;//原图
  543. $record['thumbnail_image'] = $db_thumbnail_path;//缩略图
  544. }
  545. $record['sys_rq'] = date('Y-m-d');
  546. $record['template_name'] = $params['template_name'];
  547. $record['update_time'] = date('Y-m-d H:i:s');
  548. // 更新模板记录
  549. $res = Db::name('product_template')->where('id', $templateId)->update($record);
  550. if (!$res) {
  551. return json([
  552. 'code' => 1,
  553. 'msg' => '数据库更新失败',
  554. 'data' => ''
  555. ]);
  556. }
  557. // 处理layers数据,更新模版-素材表(template_material_relation)
  558. if (!empty($params['layers'])) {
  559. // 删除旧的关联记录
  560. Db::name('template_material_relation')->where('template_id', $templateId)->delete();
  561. $layers = $params['layers'];
  562. foreach ($layers as $layer) {
  563. $materialId = $layer['material_id'] ?? null;
  564. $materialUrl = isset($layer['url']) ? $layer['url'] : '';
  565. if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
  566. $materialId = $layerIdToMaterial[$layer['id']]['id'];
  567. $materialUrl = $layerIdToMaterial[$layer['id']]['url'];
  568. }
  569. $relationData = [
  570. 'template_id' => $templateId,//模版ID
  571. 'sys_id' => $params['sys_id'] ?? '',//用户名
  572. 'material_id' => $materialId,//素材ID
  573. 'z_index' => $layer['id'],//层级
  574. 'layer_name' => $layer['name'],//图层名称
  575. 'layer_type' => $layer['type'],//类型
  576. 'material_url' => $materialUrl,//素材图片
  577. 'position_x' => $layer['x'],
  578. 'position_y' => $layer['y'],
  579. 'width' => $layer['width'],
  580. 'height' => $layer['height'],
  581. 'rotation' => $layer['rotation'],//素材旋转角度
  582. 'opacity' => $layer['opacity'],//素材透明度
  583. 'visible' => $layer['visible'],//图层是否显示
  584. 'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
  585. //文字部分参数
  586. 'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
  587. 'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
  588. 'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
  589. 'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
  590. 'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
  591. 'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
  592. 'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
  593. 'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
  594. 'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
  595. 'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
  596. 'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
  597. 'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
  598. //形状与线条
  599. 'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
  600. 'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
  601. 'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
  602. 'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
  603. 'page_index' => $layer['page_index'] ?? $layer['page_index'] ?? '',//画布分页排序
  604. 'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
  605. 'create_time' => date('Y-m-d H:i:s')
  606. ];
  607. // 插入关联记录
  608. Db::name('template_material_relation')->insert($relationData);
  609. }
  610. }
  611. return json([
  612. 'code' => 0,
  613. 'msg' => '修改成功',
  614. 'data' => '',
  615. 'template_id' => $templateId,
  616. 'template_image_url' => $db_img_path,
  617. 'template_image' => $db_thumbnail_path
  618. ]);
  619. }
  620. /**
  621. * 生成缩略图(高质量)
  622. * @param string $originalPath 原图路径
  623. * @param string $savePath 保存目录
  624. * @param string $fileName 原文件名
  625. * @return string 缩略图文件名
  626. */
  627. private function generateThumbnail($originalPath, $savePath, $fileName) {
  628. // 获取图片信息
  629. $imageInfo = getimagesize($originalPath);
  630. if (!$imageInfo) {
  631. return '';
  632. }
  633. $width = $imageInfo[0];
  634. $height = $imageInfo[1];
  635. // 计算缩略图尺寸(保持比例,最大宽度400)
  636. $maxWidth = 400;
  637. $maxHeight = 400;
  638. if ($width > $maxWidth || $height > $maxHeight) {
  639. $ratio = min($maxWidth / $width, $maxHeight / $height);
  640. $thumbWidth = round($width * $ratio);
  641. $thumbHeight = round($height * $ratio);
  642. } else {
  643. $thumbWidth = $width;
  644. $thumbHeight = $height;
  645. }
  646. // 创建缩略图画布
  647. $thumbnail = imagecreatetruecolor($thumbWidth, $thumbHeight);
  648. // 根据图片类型创建图像资源
  649. switch ($imageInfo[2]) {
  650. case IMAGETYPE_JPEG:
  651. $source = imagecreatefromjpeg($originalPath);
  652. break;
  653. case IMAGETYPE_PNG:
  654. $source = imagecreatefrompng($originalPath);
  655. // 处理PNG透明
  656. imagealphablending($thumbnail, false);
  657. imagesavealpha($thumbnail, true);
  658. $transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
  659. imagefilledrectangle($thumbnail, 0, 0, $thumbWidth, $thumbHeight, $transparent);
  660. break;
  661. case IMAGETYPE_GIF:
  662. $source = imagecreatefromgif($originalPath);
  663. break;
  664. default:
  665. return '';
  666. }
  667. if (!$source) {
  668. return '';
  669. }
  670. // 调整图片大小(使用高质量缩放)
  671. imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
  672. // 生成缩略图文件名
  673. $pathInfo = pathinfo($fileName);
  674. $thumbnailName = $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
  675. $thumbnailPath = $savePath . $thumbnailName;
  676. // 保存缩略图(使用高质量设置)
  677. switch ($imageInfo[2]) {
  678. case IMAGETYPE_JPEG:
  679. imagejpeg($thumbnail, $thumbnailPath, 95); // 95% 质量,接近原图
  680. break;
  681. case IMAGETYPE_PNG:
  682. imagepng($thumbnail, $thumbnailPath, 3); // 压缩级别 3,保持较高质量
  683. break;
  684. case IMAGETYPE_GIF:
  685. imagegif($thumbnail, $thumbnailPath);
  686. break;
  687. }
  688. // 释放资源
  689. imagedestroy($source);
  690. imagedestroy($thumbnail);
  691. return $thumbnailName;
  692. }
  693. /**
  694. * 发布模版(release=1)
  695. */
  696. public function Template_Material_Publish(){
  697. $params = $this->request->param();
  698. $record['release'] = 1;
  699. $record['update_time'] = date('Y-m-d H:i:s');
  700. $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
  701. if (!$res) {
  702. return json([
  703. 'code' => 1,
  704. 'msg' => '发布失败',
  705. 'data' => ''
  706. ]);
  707. }
  708. return json([
  709. 'code' => 0,
  710. 'msg' => '发布成功'
  711. ]);
  712. }
  713. /**
  714. * 取消发布模版(release=0)
  715. */
  716. public function Template_Material_Unpublish(){
  717. $params = $this->request->param();
  718. $record['release'] = 0;
  719. $record['update_time'] = date('Y-m-d H:i:s');
  720. $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
  721. if (!$res) {
  722. return json([
  723. 'code' => 1,
  724. 'msg' => '发布失败',
  725. 'data' => ''
  726. ]);
  727. }
  728. return json([
  729. 'code' => 0,
  730. 'msg' => '发布成功'
  731. ]);
  732. }
  733. /**
  734. * 删除模版
  735. */
  736. public function Template_Material_Delete(){
  737. $params = $this->request->param();
  738. $record['mod_rq'] = date('Y-m-d H:i:s');
  739. $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
  740. if (!$res) {
  741. return json([
  742. 'code' => 1,
  743. 'msg' => '模版删除失败',
  744. 'data' => ''
  745. ]);
  746. }
  747. return json([
  748. 'code' => 0,
  749. 'msg' => '模版删除成功'
  750. ]);
  751. }
  752. /**
  753. * 素材分类查询(一级+二级树形结构,供前端展示)
  754. */
  755. public function Material_Category_List()
  756. {
  757. $all = Db::name('template_material_category')
  758. ->field('id,category_name,parent_id,sort')
  759. ->where('status', 1)
  760. ->whereNull('mod_rq')
  761. ->order('sort', 'asc')
  762. ->select();
  763. // 分离一级(parent_id=0)和二级
  764. $level1 = [];
  765. $level2ByParent = [];
  766. foreach ($all as $row) {
  767. if ($row['parent_id'] == 0) {
  768. $row['children'] = [];
  769. $level1[] = $row;
  770. } else {
  771. $pid = $row['parent_id'];
  772. if (!isset($level2ByParent[$pid])) {
  773. $level2ByParent[$pid] = [];
  774. }
  775. $level2ByParent[$pid][] = $row;
  776. }
  777. }
  778. // 按 sort 排序一级,并把二级挂到对应一级下
  779. usort($level1, function ($a, $b) {
  780. return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
  781. });
  782. foreach ($level1 as &$item) {
  783. $item['children'] = $level2ByParent[$item['id']] ?? [];
  784. usort($item['children'], function ($a, $b) {
  785. return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
  786. });
  787. }
  788. return json(['code' => 0, 'msg' => '', 'data' => $level1]);
  789. }
  790. /**
  791. * 新增素材分类(支持一级、二级)
  792. * 一级:parent_id 不传或传 0
  793. * 二级:parent_id 传一级分类的 id
  794. * 参数:category_name(必填), parent_id(可选,默认0), sort(可选,默认0), status(可选,默认1)
  795. */
  796. public function Material_Category_Add()
  797. {
  798. $params = $this->request->param();
  799. if (empty(trim($params['category_name'] ?? ''))) {
  800. return json(['code' => 1, 'msg' => '分类名称不能为空']);
  801. }
  802. $parentId = isset($params['parent_id']) ? intval($params['parent_id']) : 0;
  803. // 二级分类:校验父分类存在且未软删除
  804. if ($parentId > 0) {
  805. $parent = Db::name('template_material_category')
  806. ->where('id', $parentId)
  807. ->where('parent_id', 0)
  808. ->whereNull('mod_rq')
  809. ->find();
  810. if (!$parent) {
  811. return json(['code' => 1, 'msg' => '父分类不存在或已删除']);
  812. }
  813. }
  814. $data = [
  815. 'category_name' => trim($params['category_name']),
  816. 'parent_id' => $parentId,
  817. 'sort' => $params['sort'] ?? 0,
  818. 'status' => $params['status'] ?? '1',
  819. 'createtime' => date('Y-m-d H:i:s')
  820. ];
  821. $id = Db::name('template_material_category')->insertGetId($data);
  822. if ($id) {
  823. return json(['code' => 0, 'msg' => '新增成功', 'data' => ['id' => $id]]);
  824. }
  825. return json(['code' => 1, 'msg' => '新增失败']);
  826. }
  827. /**
  828. * 修改素材分类
  829. * 参数:id(必填), category_name/sort/status/parent_id(可选)
  830. */
  831. public function Material_Category_Update()
  832. {
  833. $params = $this->request->param();
  834. if (empty($params['id']) || !is_numeric($params['id'])) {
  835. return json(['code' => 1, 'msg' => '参数错误']);
  836. }
  837. $data = ['updatetime' => date('Y-m-d H:i:s')];
  838. if (isset($params['category_name']) && trim($params['category_name']) !== '') {
  839. $data['category_name'] = trim($params['category_name']);
  840. }
  841. if (isset($params['parent_id'])) {
  842. $data['parent_id'] = intval($params['parent_id']);
  843. }
  844. if (isset($params['sort'])) {
  845. $data['sort'] = $params['sort'];
  846. }
  847. if (isset($params['status'])) {
  848. $data['status'] = $params['status'];
  849. }
  850. $affected = Db::name('template_material_category')
  851. ->where('id', intval($params['id']))
  852. ->whereNull('mod_rq')
  853. ->update($data);
  854. if ($affected > 0) {
  855. return json(['code' => 0, 'msg' => '修改成功']);
  856. }
  857. return json(['code' => 1, 'msg' => '记录不存在或已删除']);
  858. }
  859. /**
  860. * 删除素材分类(软删除,设置 mod_rq)
  861. */
  862. public function Material_Category_Delete()
  863. {
  864. $params = $this->request->param();
  865. if (empty($params['id']) || !is_numeric($params['id'])) {
  866. return json(['code' => 1, 'msg' => '参数错误']);
  867. }
  868. $affected = Db::name('template_material_category')
  869. ->where('id', intval($params['id']))
  870. ->update(['mod_rq' => date('Y-m-d H:i:s')]);
  871. if ($affected > 0) {
  872. return json(['code' => 0, 'msg' => '删除成功']);
  873. }
  874. return json(['code' => 1, 'msg' => '记录不存在或已删除']);
  875. }
  876. }