mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-03-26 15:23:15 +00:00
550 lines
19 KiB
PHP
550 lines
19 KiB
PHP
<?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);
|
||
}
|
||
}
|