| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- <?php
- // +----------------------------------------------------------------------
- // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
- // +----------------------------------------------------------------------
- // | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
- // +----------------------------------------------------------------------
- // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
- // +----------------------------------------------------------------------
- // | Author: CRMEB Team <admin@crmeb.com>
- // +----------------------------------------------------------------------
- namespace app\adminapi\controller\v1\diy;
- use app\adminapi\controller\AuthController;
- use app\jobs\ThemeExportJob;
- use app\services\activity\coupon\StoreCouponIssueServices;
- use app\services\article\ArticleServices;
- use app\services\diy\ThemeDownloadServices;
- use app\services\diy\ThemeServices;
- use app\services\product\product\StoreProductServices;
- use SplFileInfo;
- use think\facade\App;
- /**
- * 主题管理控制器
- * 处理主题的列表、详情、保存、导入导出等功能
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- class Theme extends AuthController
- {
- /**
- * @var ThemeServices 主题服务类
- */
- protected $services;
- /**
- * 构造方法
- * 注入 ThemeServices 服务
- * @param App $app 应用容器实例
- * @param ThemeServices $services 主题服务实例
- */
- public function __construct(App $app, ThemeServices $services)
- {
- parent::__construct($app);
- $this->services = $services;
- }
- /**
- * 获取主题列表
- * 支持根据标题、类型、状态筛选
- * @return \think\Response JSON格式的响应
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- public function getThemeList()
- {
- // 获取请求参数,设置默认值
- $where = $this->request->getMore([
- ['title', ''],
- ['type', ''],
- ['page_type', ''],
- ['is_del', 0],
- ]);
- // 调用服务层获取列表数据
- $data = $this->services->getThemeList($where);
- return app('json')->success($data);
- }
- /**
- * 获取主题详情
- * @param int $id 主题ID
- * @param string $type 查询类型(可选)
- * @return \think\Response JSON格式的响应
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- public function getThemeInfo($id, $type = '')
- {
- $data = $this->services->getThemeInfo($id, $type);
- return app('json')->success($data);
- }
- /**
- * 保存主题基本信息
- * @param int $id 主题ID
- * @return \think\Response JSON格式的响应
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- public function saveTheme($id)
- {
- $data = $this->request->getMore([
- ['tid', 0],
- ['title', ''],
- ['type', ''],
- ['value', ''],
- ['page_type', 'theme'],
- ]);
- $id = $this->services->saveTheme($id, $data);
- return app('json')->success('保存成功', ['id' => $id]);
- }
- /**
- * 保存主题标题信息
- * @param int $id 主题ID
- * @return \think\Response JSON格式的响应
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- public function saveThemeTitle($id)
- {
- $data = $this->request->getMore([
- ['tid', 0],
- ['title', ''],
- ['info', ''],
- ['page_type', 'theme'],
- ]);
- $id = $this->services->saveThemeTitle($id, $data);
- return app('json')->success('保存成功', ['id' => $id]);
- }
- /**
- * 保存主题图片信息
- * @param int $id 主题ID
- * @return \think\Response JSON格式的响应
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2025/12/18
- */
- public function saveThemeImage($id)
- {
- $data = $this->request->getMore([
- ['image', ''],
- ['type', ''],
- ]);
- $id = $this->services->saveThemeImage($id, $data);
- return app('json')->success('保存成功', ['id' => $id]);
- }
- /**
- * 获取自定义组件-文章列表
- * 用于DIY页面选择文章组件的数据源
- * @return \think\Response JSON格式的响应
- * @throws \ReflectionException
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/1/12
- */
- public function getThemeArticleList()
- {
- $where = $this->request->getMore([
- ['ids', ''],
- ['cid', ''],
- ['order', 0],
- ['sort', 0],
- ['limit', 10],
- ]);
- $data = app()->make(ArticleServices::class)->getThemeArticle($where);
- return app('json')->success($data);
- }
- /**
- * 获取自定义组件-优惠券列表
- * 用于DIY页面选择优惠券组件的数据源
- * @return \think\Response JSON格式的响应
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/1/13
- */
- public function getThemeCouponList()
- {
- $where = $this->request->getMore([
- ['ids', ''],
- ['type', ''],
- ['user_type', ''],
- ['send_type', ''],
- ['is_min_price', 0],
- ['min_price', 0],
- ['start_time', ''],
- ['end_time', ''],
- ['order', 0],
- ['sort', 0],
- ['limit', 10],
- ]);
- $data = app()->make(StoreCouponIssueServices::class)->getThemeCoupon($where);
- return app('json')->success($data);
- }
- /**
- * 获取自定义组件-商品列表
- * 用于DIY页面选择商品组件的数据源
- * @return \think\Response JSON格式的响应
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/1/13
- */
- public function getThemeProductList()
- {
- $where = $this->request->getMore([
- ['ids', ''],
- ['cate_ids', ''],
- ['order', 0],
- ['sort', 0],
- ['limit', 10],
- ]);
- $data = app()->make(StoreProductServices::class)->getThemeProduct($where);
- return app('json')->success($data);
- }
- /**
- * 导出主题数据
- * 1. 在 eb_theme_download 中写入一条待处理记录(不含 download_url)
- * 2. 将实际打包任务推入队列异步执行
- * 3. 队列完成后回填 download_url
- *
- * @param int $id 主题ID
- * @return \think\Response
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/3/10
- */
- public function exportTheme($id)
- {
- // 判断是否使用 Redis 缓存且开启了消息队列
- $queueEnabled = sys_config('queue_open', 0) == 1 && \think\facade\Env::get('cache.driver', 'file') == 'redis';
- if (!$queueEnabled) {
- return app('json')->fail('导出功能需要开启 Redis 缓存并开启消息队列,请先到系统设置中开启对应配置');
- }
- $id = (int)$id;
- // 1. 查询主题基本信息,获取标题
- $info = $this->services->getThemeInfo($id);
- // 2. 创建主题打包目录
- $dir = public_path() . 'theme/download/' . $id . '/';
- if (!is_dir($dir)) mkdir($dir, 0755, true);
- // 3. 清理打包目录,防止数据污染
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
- \RecursiveIteratorIterator::CHILD_FIRST
- );
- foreach ($iterator as $fileInfo) {
- if ($fileInfo->isDir()) {
- @rmdir($fileInfo->getRealPath());
- } else {
- @unlink($fileInfo->getRealPath());
- }
- }
- // 4. 创建主题图片目录
- $imagesDir = $dir . 'images/';
- if (!is_dir($imagesDir)) mkdir($imagesDir, 0755, true);
- // 5. 向 eb_theme_download 写入待处理记录(download_url 暂不填写)
- /** @var ThemeDownloadServices $themeDownloadServices */
- $themeDownloadServices = app()->make(ThemeDownloadServices::class);
- $recordId = $themeDownloadServices->addDownloadRecord($id, $info['title'], '');
- // 6. 将打包任务推入队列
- ThemeExportJob::dispatch('export', [$info, $recordId]);
- return app('json')->success('正在导出中,请勿操作页面!', ['record_id' => $recordId]);
- }
- /**
- * 查询主题导出记录
- * 前端轮询该接口,待 download_url 不为空时说明队列已完成
- *
- * @param int $record_id 下载记录ID
- * @return \think\Response
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/3/10
- */
- public function getExportRecord($record_id)
- {
- /** @var ThemeDownloadServices $themeDownloadServices */
- $themeDownloadServices = app()->make(ThemeDownloadServices::class);
- $record = $themeDownloadServices->getDownloadInfo((int)$record_id);
- return app('json')->success([
- 'download_url' => $record['download_url'] ?? '',
- ]);
- }
- /**
- * 导入主题
- * 上传 Zip 包,解压并还原主题配置
- * 1. 解压 Zip 包到 theme/import/
- * 2. 读取 config.json
- * 3. 提取包内图片移动到 uploads/theme/ 目录
- * 4. 递归遍历配置,修正图片路径并替换域名
- *
- * @return \think\Response
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/1/15
- */
- public function importTheme()
- {
- // 1 获取文件
- [$importUrl] = $this->request->postMore([
- ['url', ''],
- ], true);
- $realPath = public_path() . $importUrl;
- if (!file_exists($realPath)) return app('json')->fail('文件不存在');
- // 2. 解压文件到 theme/import/ 目录
- $dir = 'theme/import/';
- if (!is_dir($dir)) mkdir($dir, 0755, true);
- // 清理导入目录,防止数据污染
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
- \RecursiveIteratorIterator::CHILD_FIRST
- );
- /** @var SplFileInfo $fileInfo */
- foreach ($iterator as $fileInfo) {
- if ($fileInfo->isDir()) {
- @rmdir($fileInfo->getRealPath());
- } else {
- @unlink($fileInfo->getRealPath());
- }
- }
- $zip = new \ZipArchive();
- $zip->open($realPath);
- $zip->extractTo($dir);
- $zip->close();
- // 3. 读取解压后的 config.json 文件
- $configPath = $dir . 'config.json';
- if (!file_exists($configPath)) return app('json')->fail('文件不存在');
- $config = json_decode(file_get_contents($configPath), true);
- if (!is_array($config)) return app('json')->fail('文件内容错误');
- // 4. 处理图片资源迁移
- // 将压缩包里面的所有图片,移动到 uploads/theme/{时间戳}/ 文件夹下
- $timestamp = date('YmdHis');
- $themeDir = 'uploads/theme/' . $timestamp . '/';
- if (!is_dir($themeDir)) mkdir($themeDir, 0755, true);
- $rootPath = realpath($dir);
- $imageMap = []; // 记录相对路径到新上传路径的映射
- // 递归扫描解压目录中的所有图片
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
- );
- foreach ($iterator as $fileInfo) {
- if ($fileInfo->isDir()) continue;
- $filePath = $fileInfo->getRealPath();
- // 跳过配置文件
- if (basename($filePath) === 'config.json') continue;
- // 只处理指定扩展名的图片
- if (!preg_match('/\.(png|jpe?g|gif|webp|svg)$/i', $filePath)) continue;
- // 获取文件相对于解压根目录的相对路径
- $relative = ltrim(str_replace($rootPath, '', $filePath), DIRECTORY_SEPARATOR);
- $basename = basename($filePath);
- // 目标路径
- $target = $themeDir . $basename;
- // 移动/复制文件
- if (@copy($filePath, $target)) {
- $imageMap[$relative] = $target; // 记录映射:包内相对路径 => 新系统路径
- }
- }
- // 5. 准备域名替换的基础 URL
- $siteUrl = rtrim(sys_config('site_url'), '/');
- $siteParts = parse_url($siteUrl);
- $base = '';
- if ($siteParts && isset($siteParts['host'])) {
- $scheme = $siteParts['scheme'] ?? 'http';
- $base = $scheme . '://' . $siteParts['host'];
- if (isset($siteParts['port'])) {
- $base .= ':' . $siteParts['port'];
- }
- }
- $rewriteArray = function (&$data) use (&$rewriteArray, $imageMap, $base) {
- if (!is_array($data)) return;
- foreach ($data as $k => &$v) {
- if (is_array($v)) {
- $rewriteArray($v);
- } elseif (is_string($v)) {
- $decoded = json_decode($v, true);
- if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
- $rewriteArray($decoded);
- $v = json_encode($decoded, JSON_UNESCAPED_UNICODE);
- continue;
- }
- // 检查是否在图片映射表中(处理本地导入的图片)
- // 如果在映射表中,说明该图片已从压缩包解压并上传到 uploads/theme/ 目录
- // 此时需要将其路径替换为带当前站点域名的完整 URL
- if (isset($imageMap[$v])) {
- if ($base !== '') {
- // 拼接域名 + 新路径
- $v = rtrim($base, '/') . '/' . ltrim($imageMap[$v], '/');
- } else {
- // 如果无法获取域名,则仅使用相对路径
- $v = $imageMap[$v];
- }
- continue;
- }
- // 兼容处理 exportTheme 导出时带有的 theme/download/ 前缀
- // 导出时,home_image 等字段被赋值为 theme/download/xxx.png
- // 而压缩包内的文件实际上是 xxx.png,导致直接匹配 imageMap 失败
- // 因此需要去掉 theme/download/ 前缀再次尝试匹配
- if (strpos($v, 'theme/download/') === 0) {
- $rel = substr($v, strlen('theme/download/'));
- if (isset($imageMap[$rel])) {
- if ($base !== '') {
- // 同样拼接域名 + 新路径
- $v = rtrim($base, '/') . '/' . ltrim($imageMap[$rel], '/');
- } else {
- $v = $imageMap[$rel];
- }
- continue;
- }
- }
- // 处理域名替换(将旧域名的链接替换为当前站点域名)
- // 防止导入的主题配置中包含旧站点的域名,导致图片无法加载
- $parts = parse_url($v);
- if (!$parts || !isset($parts['host']) || $base === '') {
- continue;
- }
- $path = $parts['path'] ?? '';
- $query = isset($parts['query']) ? '?' . $parts['query'] : '';
- $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
- $v = rtrim($base, '/') . $path . $query . $fragment;
- }
- }
- unset($v);
- };
- // 执行替换逻辑
- $rewriteArray($config);
- // 执行数据写入
- $themeId = $this->services->importThemeData($config);
- // 返回成功
- return app('json')->success('导入成功', ['theme_id' => $themeId]);
- }
- /**
- * @description: 使用主题
- * @param int $id 主题ID
- * @return array
- */
- public function useTheme(int $id)
- {
- $this->services->useTheme($id);
- return app('json')->success('使用成功');
- }
- /**
- * @description: 使用主题数据
- * @param array $data 主题数据
- * @return array
- */
- public function useThemeData($id)
- {
- [$theme_id, $type] = $this->request->getMore([
- ['theme_id', 0],
- ['type', ''],
- ], true);
- $this->services->useThemeData($id, $theme_id, $type);
- return app('json')->success('使用成功');
- }
- /**
- * @description: 获取正在使用的主题
- * @return array
- */
- public function getUsingTheme()
- {
- $theme = $this->services->getUsingTheme();
- return app('json')->success($theme);
- }
- /**
- * @description: 还原主题
- * @param int $id 主题ID
- * @return array
- */
- public function restoreTheme(int $id)
- {
- $this->services->restoreTheme($id);
- return app('json')->success('还原成功');
- }
- /**
- * @description: 删除主题
- * @param int $id 主题ID
- * @return array
- */
- public function deleteTheme(int $id)
- {
- $this->services->deleteTheme($id);
- return app('json')->success('删除成功');
- }
- /**
- * 获取微页面数据列表
- * @return \think\Response JSON格式的响应
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @author wuhaotian
- * @email 442384644@qq.com
- * @date 2026/1/20
- */
- public function getMicroPageList()
- {
- $data = $this->services->getMicroPageList();
- return app('json')->success($data);
- }
- }
|