ThemeServices.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\services\diy;
  12. use app\dao\diy\ThemeDao;
  13. use app\services\BaseServices;
  14. use crmeb\exceptions\AdminException;
  15. use crmeb\exceptions\ApiException;
  16. /**
  17. * 主题服务类
  18. *
  19. * 功能概述:
  20. * 负责系统主题的管理,包括主题的增删改查、导入导出、应用切换等功能。
  21. * 提供了对首页、分类页、详情页、个人中心等页面数据的独立管理和组合使用能力。
  22. *
  23. * 主要功能:
  24. * 1. 主题管理 - 主题列表查询、详情获取、创建与编辑
  25. * 2. 主题应用 - 切换当前使用的主题,或单独应用某个主题的特定页面数据
  26. * 3. 数据导入 - 支持导入外部主题配置数据
  27. * 4. 资源管理 - 管理主题相关的图片、标题等资源
  28. * 5. 版本控制 - 记录主题数据的更新时间和版本信息
  29. *
  30. * @package app\services\diy
  31. * @author wuhaotian
  32. * @email 442384644@qq.com
  33. * @date 2025/12/18
  34. */
  35. class ThemeServices extends BaseServices
  36. {
  37. /**
  38. * 构造函数 - 初始化依赖
  39. *
  40. * 注入 ThemeDao 依赖,用于数据库操作。
  41. *
  42. * @param ThemeDao $dao 主题数据访问对象
  43. * @author wuhaotian
  44. * @email 442384644@qq.com
  45. * @date 2026/02/03
  46. */
  47. public function __construct(ThemeDao $dao)
  48. {
  49. $this->dao = $dao;
  50. }
  51. /**
  52. * 获取主题列表
  53. *
  54. * 功能概述:
  55. * 根据传入的查询条件,分页获取主题列表数据,并对返回的数据进行格式化处理。
  56. * 处理内容包括:时间戳转日期字符串、图片路径转完整URL、JSON数据解析等。
  57. *
  58. * @param array $where 查询条件数组
  59. * @return array 包含列表数据 list 和总数 count 的数组
  60. * @throws \think\db\exception\DataNotFoundException
  61. * @throws \think\db\exception\DbException
  62. * @throws \think\db\exception\ModelNotFoundException
  63. * @author wuhaotian
  64. * @email 442384644@qq.com
  65. * @date 2026/02/03
  66. */
  67. public function getThemeList($where)
  68. {
  69. [$page, $limit] = $this->getPageValue();
  70. $field = 'id,title,info,type,home_image,category_image,detail_image,user_image,theme_data,add_time,up_time,is_use,page_type';
  71. $order = 'id desc';
  72. if (($where['page_type'] ?? '') === 'all') {
  73. unset($where['page_type']);
  74. } else {
  75. $where['page_type'] = 'theme';
  76. }
  77. $list = $this->dao->themeList($where, $field, $page, $limit, $order);
  78. foreach ($list as &$item) {
  79. if (isset($item['add_time'])) $item['add_time'] = date('Y-m-d H:i', $item['add_time']);
  80. if (isset($item['up_time'])) $item['up_time'] = date('Y-m-d H:i', $item['up_time']);
  81. if (isset($item['home_data_update_time'])) $item['home_data_update_time'] = date('Y-m-d H:i', $item['home_data_update_time']);
  82. if (isset($item['category_data_update_time'])) $item['category_data_update_time'] = date('Y-m-d H:i', $item['category_data_update_time']);
  83. if (isset($item['detail_data_update_time'])) $item['detail_data_update_time'] = date('Y-m-d H:i', $item['detail_data_update_time']);
  84. if (isset($item['user_data_update_time'])) $item['user_data_update_time'] = date('Y-m-d H:i', $item['user_data_update_time']);
  85. if (isset($item['theme_data_update_time'])) $item['theme_data_update_time'] = date('Y-m-d H:i', $item['theme_data_update_time']);
  86. if (isset($item['type'])) $item['type'] = $item['type'] == 0 ? '自建主题' : '广场主题';
  87. if (isset($item['theme_data'])) $item['theme_data'] = json_decode($item['theme_data'], true) ?? [];
  88. $item['home_image'] = set_file_url($item['home_image']);
  89. $item['category_image'] = set_file_url($item['category_image']);
  90. $item['detail_image'] = set_file_url($item['detail_image']);
  91. $item['user_image'] = set_file_url($item['user_image']);
  92. }
  93. $count = $this->dao->themeCount($where);
  94. return compact('list', 'count');
  95. }
  96. /**
  97. * 获取主题版本号
  98. *
  99. * 功能概述:
  100. * 根据主题ID获取该主题的当前版本号。
  101. * 如果ID为0,则获取当前正在使用的主题的版本号。
  102. *
  103. * @param int $id 主题ID,0表示当前使用的主题
  104. * @return mixed 版本号字符串
  105. * @author wuhaotian
  106. * @email 442384644@qq.com
  107. * @date 2026/02/03
  108. */
  109. public function getThemeVersion($id)
  110. {
  111. $where = $id == 0 ? ['is_use' => 1] : ['id' => $id];
  112. return $this->dao->value($where, 'version');
  113. }
  114. /**
  115. * 获取主题信息
  116. *
  117. * 功能概述:
  118. * 根据主题ID和类型获取主题的详细信息。
  119. * 支持获取全部信息或指定类型(如首页、分类页、详情页等)的数据。
  120. * 对返回的数据进行必要的格式化和默认值填充。
  121. *
  122. * 返回数据结构:
  123. * 根据 $type 不同返回不同结构:
  124. * - 'all'/'base': 返回主题完整记录数组
  125. * - 'home'/'detail'/'user'/'theme': 返回解析后的配置数组
  126. * - 'category': 返回包含 status 的数组
  127. *
  128. * @param int $id 主题ID,0表示当前使用的主题
  129. * @param string $type 数据类型:all, home, category, detail, user, theme, base
  130. * @return array|int[]|mixed|\think\Model|null
  131. * @throws \think\db\exception\DataNotFoundException
  132. * @throws \think\db\exception\DbException
  133. * @throws \think\db\exception\ModelNotFoundException
  134. * @throws AdminException 数据不存在时抛出
  135. * @author wuhaotian
  136. * @email 442384644@qq.com
  137. * @date 2026/02/03
  138. */
  139. public function getThemeInfo($id, $type = 'all')
  140. {
  141. $where = $id == 0 ? ['is_use' => 1] : ['id' => $id];
  142. $info = $this->dao->get($where);
  143. if (!$info) throw new AdminException('数据不存在');
  144. $info = $info->toArray();
  145. if ($type == 'home') {
  146. return json_decode($info['home_data'], true) ?? [];
  147. } elseif ($type == 'category') {
  148. return ['status' => $info['category_data'] ?? 1];
  149. } elseif ($type == 'detail') {
  150. return json_decode($info['detail_data'], true) ?? [];
  151. } elseif ($type == 'user') {
  152. return json_decode($info['user_data'], true) ?? [];
  153. } elseif ($type == 'theme') {
  154. if ($info['theme_data'] == '' || $info['theme_data'] == null || $info['theme_data'] == 'null') {
  155. $info['theme_data'] = '{"theme_color":"#E93323","gradient_color":"#FF7931","sub_color":"#FE960F","light_color":"rgba(233, 51, 35, 0.1)"}';
  156. }
  157. return json_decode($info['theme_data'], true) ?? [];
  158. } elseif ($type == 'base') {
  159. return ['id' => $info['id'], 'type' => $info['type'], 'title' => $info['title'], 'info' => $info['info']];
  160. } else {
  161. $info['home_data_update_time'] = date('Y-m-d H:i:s', $info['home_data_update_time']);
  162. $info['category_data_update_time'] = date('Y-m-d H:i:s', $info['category_data_update_time']);
  163. $info['detail_data_update_time'] = date('Y-m-d H:i:s', $info['detail_data_update_time']);
  164. $info['user_data_update_time'] = date('Y-m-d H:i:s', $info['user_data_update_time']);
  165. $info['theme_data_update_time'] = date('Y-m-d H:i:s', $info['theme_data_update_time']);
  166. $info['add_time'] = date('Y-m-d H:i:s', $info['add_time']);
  167. $info['up_time'] = date('Y-m-d H:i:s', $info['up_time']);
  168. $ids = [$info['home_data_id'], $info['category_data_id'], $info['detail_data_id'], $info['user_data_id'], $info['theme_data_id']];
  169. $titles = $this->dao->getColumn([['id', 'in', $ids]], 'title', 'id');
  170. $info['home_data_id_title'] = $titles[$info['home_data_id']] ?? '';
  171. $info['category_data_id_title'] = $titles[$info['category_data_id']] ?? '';
  172. $info['detail_data_id_title'] = $titles[$info['detail_data_id']] ?? '';
  173. $info['user_data_id_title'] = $titles[$info['user_data_id']] ?? '';
  174. $info['theme_data_id_title'] = $titles[$info['theme_data_id']] ?? '';
  175. return $info;
  176. }
  177. }
  178. /**
  179. * 保存主题数据
  180. *
  181. * 功能概述:
  182. * 创建新主题或更新现有主题的数据。
  183. * 支持从模板主题复制数据创建新主题。
  184. * 根据不同的页面类型(home, category, detail, user, theme)处理相应的数据保存逻辑。
  185. * 自动更新版本号和最后修改时间。
  186. *
  187. * @param int $id 主题主键,0 表示新增
  188. * @param array $data 待保存数据,必须包含 type、value,可选 tid、title
  189. * @return int 新增或更新后的主题 ID
  190. * @throws AdminException 当指定 tid 但主题不存在时抛出
  191. * @author wuhaotian
  192. * @email 442384644@qq.com
  193. * @date 2026/02/03
  194. */
  195. public function saveTheme($id, $data)
  196. {
  197. // 初始化待写入数组
  198. $saveData = [];
  199. // 如果指定了模板主题 ID(tid),则先复制其数据作为基础
  200. if ($data['tid'] !== 0) {
  201. // 查询模板主题
  202. $tInfo = $this->dao->get($data['tid']);
  203. if (!$tInfo) {
  204. throw new AdminException('主题不存在');
  205. }
  206. // 将模板主题数据转为数组,并剔除主键 id,避免冲突
  207. $saveData = $tInfo->toArray();
  208. // 新主题默认未启用
  209. $saveData['is_use'] = 0;
  210. unset($saveData['id']);
  211. }
  212. // 如果传入了标题,则覆盖
  213. if ($data['title'] != '') {
  214. $saveData['title'] = $data['title'];
  215. }
  216. if ($id == 0) {
  217. $type = 0;
  218. $saveData['category_data'] = 1;
  219. $saveData['category_data_update_time'] = time();
  220. $saveData['category_image'] = '/statics/images/cate1.png';
  221. } else {
  222. $type = $this->dao->value(['id' => $id], 'type');
  223. }
  224. // 将传入的 value 统一转为 JSON 字符串
  225. $value = json_encode($data['value']);
  226. // 根据模块类型分别处理数据、预览图及更新时间
  227. switch ($data['type']) {
  228. case 'home':
  229. // 首页
  230. $saveData['home_data'] = $value;
  231. $saveData['home_data_update_time'] = time();
  232. // 自建主题需要同步写入默认数据
  233. if ($type == 0) {
  234. $saveData['home_default_data'] = $value;
  235. }
  236. break;
  237. case 'category':
  238. // 分类页
  239. $saveData['category_data'] = $value;
  240. $saveData['category_data_update_time'] = time();
  241. // 根据 value 生成对应预览图路径
  242. $saveData['category_image'] = '/statics/images/cate' . $value . '.png';
  243. if ($type == 0) {
  244. $saveData['category_default_data'] = $value;
  245. $saveData['category_default_image'] = '/statics/images/cate' . $value . '.png';
  246. }
  247. break;
  248. case 'detail':
  249. // 详情页
  250. $saveData['detail_data'] = $value;
  251. $saveData['detail_data_update_time'] = time();
  252. if ($type == 0) {
  253. $saveData['detail_default_data'] = $value;
  254. }
  255. break;
  256. case 'user':
  257. // 用户中心
  258. $saveData['user_data'] = $value;
  259. $saveData['user_data_update_time'] = time();
  260. if ($type == 0) {
  261. $saveData['user_default_data'] = $value;
  262. }
  263. break;
  264. case 'theme':
  265. // 主题自身数据
  266. $saveData['theme_data'] = $value;
  267. $saveData['theme_data_update_time'] = time();
  268. if ($type == 0) {
  269. $saveData['theme_default_data'] = $value;
  270. }
  271. break;
  272. }
  273. // 每次保存都生成新的版本号
  274. $saveData['version'] = uniqid();
  275. // 新增 or 更新
  276. if ($id) {
  277. // 更新
  278. $saveData['up_time'] = time();
  279. $this->dao->update($id, $saveData);
  280. } else {
  281. // 新增
  282. $saveData['page_type'] = $data['page_type'];
  283. $saveData['add_time'] = time();
  284. $saveData['up_time'] = time();
  285. $id = $this->dao->insertGetId($saveData);
  286. }
  287. // 返回最终主题 ID
  288. return $id;
  289. }
  290. /**
  291. * 保存主题标题信息
  292. *
  293. * 功能概述:
  294. * 更新主题的标题和简介信息,或创建新的主题记录(仅包含标题信息)。
  295. * 更新操作会同步更新版本号和最后修改时间。
  296. *
  297. * @param int $id 主题ID,0表示新增
  298. * @param array $data 包含 title 和 info 的数据数组
  299. * @return int|mixed|string 主题ID
  300. * @author wuhaotian
  301. * @email 442384644@qq.com
  302. * @date 2026/02/03
  303. */
  304. public function saveThemeTitle($id, $data)
  305. {
  306. // 如果指定了模板主题 ID(tid),则先复制其数据作为基础
  307. if ($data['tid'] !== 0) {
  308. // 查询模板主题
  309. $tInfo = $this->dao->get($data['tid']);
  310. if (!$tInfo) {
  311. throw new AdminException('主题不存在');
  312. }
  313. // 将模板主题数据转为数组,并剔除主键 id,避免冲突
  314. $saveData = $tInfo->toArray();
  315. // 新主题默认未启用
  316. $saveData['is_use'] = 0;
  317. unset($saveData['id']);
  318. }
  319. $saveData['title'] = $data['title'];
  320. $saveData['info'] = $data['info'];
  321. $saveData['version'] = uniqid();
  322. $saveData['page_type'] = $data['page_type'];
  323. if ($id) {
  324. $saveData['up_time'] = time();
  325. $this->dao->update($id, $saveData);
  326. } else {
  327. $saveData['add_time'] = time();
  328. $saveData['up_time'] = time();
  329. $id = $this->dao->insertGetId($saveData);
  330. }
  331. return $id;
  332. }
  333. /**
  334. * 保存主题图片信息
  335. *
  336. * 功能概述:
  337. * 更新主题各模块(首页、详情页、用户中心)的预览图片。
  338. * 如果是默认主题(type=0),会同步更新默认图片配置。
  339. * 自动更新版本号和最后修改时间。
  340. *
  341. * @param int $id 主题ID
  342. * @param array $data 包含 type (home/detail/user) 和 image 的数据数组
  343. * @return int|mixed|string 主题ID
  344. * @author wuhaotian
  345. * @email 442384644@qq.com
  346. * @date 2025/12/18
  347. */
  348. public function saveThemeImage($id, $data)
  349. {
  350. $type = $id ? $this->dao->value(['id' => $id], 'type') : 0;
  351. switch ($data['type']) {
  352. case 'home':
  353. $saveData['home_image'] = $data['image'];
  354. if ($type == 0) $saveData['home_default_image'] = $data['image'];
  355. break;
  356. case 'detail':
  357. $saveData['detail_image'] = $data['image'];
  358. if ($type == 0) $saveData['detail_default_image'] = $data['image'];
  359. break;
  360. case 'user':
  361. $saveData['user_image'] = $data['image'];
  362. if ($type == 0) $saveData['user_default_image'] = $data['image'];
  363. break;
  364. }
  365. $saveData['version'] = uniqid();
  366. if ($id) {
  367. $saveData['up_time'] = time();
  368. $this->dao->update($id, $saveData);
  369. } else {
  370. $saveData['page_type'] = 'theme';
  371. $saveData['add_time'] = time();
  372. $saveData['up_time'] = time();
  373. $id = $this->dao->insertGetId($saveData);
  374. }
  375. return $id;
  376. }
  377. /**
  378. * 导入主题数据
  379. *
  380. * 功能概述:
  381. * 将外部导入的主题配置数据保存到数据库中。
  382. * 包含主题的所有页面配置(首页、分类、详情、个人中心)及其对应的默认配置。
  383. *
  384. * @param array $config 主题配置数据数组
  385. * @return mixed 新增的主题ID
  386. * @author wuhaotian
  387. * @email 442384644@qq.com
  388. * @date 2026/02/03
  389. */
  390. public function importThemeData($config)
  391. {
  392. $data = [];
  393. $data['version'] = uniqid(); // 版本号
  394. $data['title'] = $config['title']; // 标题
  395. $data['info'] = $config['info']; // 简介
  396. $data['type'] = 1; // 类型
  397. $data['home_data'] = $data['home_default_data'] = $config['home_data']; // 首页数据
  398. $data['home_image'] = $data['home_default_image'] = $config['home_image']; // 首页封面
  399. $data['home_data_id'] = $config['home_data_id']; // 首页数据ID
  400. $data['home_data_update_time'] = time(); // 首页数据更新时间
  401. $data['category_data'] = $data['category_default_data'] = $config['category_data']; // 分类页数据
  402. $data['category_image'] = $data['category_default_image'] = $config['category_image']; // 分类页封面
  403. $data['category_data_id'] = $config['category_data_id']; // 分类页数据ID
  404. $data['category_data_update_time'] = time(); // 分类页数据更新时间
  405. $data['detail_data'] = $data['detail_default_data'] = $config['detail_data']; // 详情页数据
  406. $data['detail_image'] = $data['detail_default_image'] = $config['detail_image']; // 详情页封面
  407. $data['detail_data_id'] = $config['detail_data_id']; // 详情页数据ID
  408. $data['detail_data_update_time'] = time(); // 详情页数据更新时间
  409. $data['user_data'] = $data['user_default_data'] = $config['user_data']; // 个人中心数据
  410. $data['user_image'] = $data['user_default_image'] = $config['user_image']; // 个人中心封面
  411. $data['user_data_id'] = $config['user_data_id']; // 个人中心数据ID
  412. $data['user_data_update_time'] = time(); // 个人中心数据更新时间
  413. $data['theme_data'] = $data['theme_default_data'] = json_encode($config['theme_data'], JSON_UNESCAPED_UNICODE); // 主题数据
  414. $data['theme_data_id'] = $config['theme_data_id']; // 主题数据ID
  415. $data['theme_data_update_time'] = time(); // 主题数据更新时间
  416. $data['page_type'] = 'theme'; // 页面类型
  417. $data['is_use'] = 0; // 是否使用
  418. $data['is_del'] = 0; // 是否删除
  419. $data['add_time'] = time(); // 添加时间
  420. $data['up_time'] = time(); // 更新时间
  421. $id = $this->dao->insertGetId($data);
  422. return $id;
  423. }
  424. /**
  425. * 使用主题
  426. *
  427. * 功能概述:
  428. * 将指定主题设置为当前启用状态。
  429. * 该操作会先将所有主题设为未启用,然后启用指定ID的主题。
  430. *
  431. * @param int $id 要启用的主题ID
  432. * @return bool 操作成功返回 true
  433. * @author wuhaotian
  434. * @email 442384644@qq.com
  435. * @date 2026/02/03
  436. */
  437. public function useTheme(int $id)
  438. {
  439. $this->dao->update(['is_use' => 1], ['is_use' => 0]);
  440. $this->dao->update($id, ['is_use' => 1]);
  441. return true;
  442. }
  443. /**
  444. * 使用主题数据
  445. *
  446. * 功能概述:
  447. * 将某个主题的特定模块数据(如首页、详情页等)应用到目标主题数据记录中。
  448. * 实现主题数据的局部复用或混搭。
  449. *
  450. * @param int $id 目标主题数据ID
  451. * @param int $theme_id 源主题ID
  452. * @param string $type 数据类型(home/category/detail/user/theme)
  453. * @return bool 操作成功返回 true
  454. * @throws AdminException 当源主题数据不存在时抛出
  455. * @author wuhaotian
  456. * @email 442384644@qq.com
  457. * @date 2026/02/03
  458. */
  459. public function useThemeData(int $id, int $theme_id, string $type)
  460. {
  461. $data = $this->dao->get(['id' => $theme_id]);
  462. if (!$data) throw new AdminException('主题数据不存在');
  463. $this->dao->update(['id' => $id], [$type . '_data_update_time' => time(), $type . '_image' => $data[$type . '_image'], $type . '_data' => $data[$type . '_data'], $type . '_data_id' => $theme_id]);
  464. return true;
  465. }
  466. /**
  467. * 获取当前正在使用的主题信息
  468. *
  469. * 功能概述:
  470. * 查询当前启用的主题(is_use=1),并聚合其关联的各模块(首页、分类、详情等)数据。
  471. * 如果采用了混搭模式(引用了其他主题的模块),会解析出实际来源主题的标题和图片信息。
  472. *
  473. * 返回数据结构:
  474. * - id, title, info, version: 主题基础信息
  475. * - confuse: 是否混搭模式 (0/1)
  476. * - data_info: 各模块详情列表(包含 key, title, image, update_time)
  477. * - theme_data: 主题全局样式配置
  478. *
  479. * @return array 返回包含主题基础信息及各个模块详细配置的数据
  480. * @throws AdminException 当没有正在使用的主题时抛出异常
  481. * @author wuhaotian
  482. * @email 442384644@qq.com
  483. * @date 2026/02/03
  484. */
  485. public function getUsingTheme()
  486. {
  487. // 查询当前正在使用的主题记录(is_use = 1)
  488. $data = $this->dao->get(['is_use' => 1]);
  489. if (!$data) throw new AdminException('没有正在使用的主题');
  490. // 收集各模块关联的主题ID,并过滤掉空值
  491. $themeIds = array_filter([
  492. $data['home_data_id'], // 首页模块关联主题ID
  493. $data['category_data_id'], // 分类页模块关联主题ID
  494. $data['detail_data_id'], // 详情页模块关联主题ID
  495. $data['user_data_id'], // 用户中心模块关联主题ID
  496. $data['theme_data_id'], // 主题自身数据关联主题ID
  497. ]);
  498. // 组装最终返回的主题信息数组
  499. $theme = [];
  500. $theme['id'] = $data['id']; // 主题ID
  501. $theme['title'] = $data['title']; // 主题名称
  502. $theme['info'] = $data['info']; // 主题简介
  503. $theme['version'] = $data['version']; // 主题版本号
  504. $theme['confuse'] = 0; // 是否混搭使用主题:0否 1是
  505. // 若存在关联主题ID,则批量查询其标题,供后续拼接使用
  506. if ($themeIds) {
  507. $themeData = $this->dao->getColumn([['id', 'in', $themeIds]], 'title', 'id');
  508. $theme['confuse'] = 1;
  509. }
  510. $theme['data_info'] = [
  511. [
  512. 'key' => 'home',
  513. 'title' => $themeData[$data['home_data_id']] ?? $data['title'], // 首页模块标题(优先取关联主题标题)
  514. 'image' => set_file_url($data['home_image']), // 首页预览图
  515. 'update_time' => date('Y-m-d H:i:s', $data['home_data_update_time']), // 首页数据更新时间
  516. ],
  517. [
  518. 'key' => 'category',
  519. 'title' => $themeData[$data['category_data_id']] ?? $data['title'], // 分类页模块标题(优先取关联主题标题)
  520. 'image' => set_file_url($data['category_image']), // 分类页预览图
  521. 'update_time' => date('Y-m-d H:i:s', $data['category_data_update_time']), // 分类页数据更新时间
  522. ],
  523. [
  524. 'key' => 'detail',
  525. 'title' => $themeData[$data['detail_data_id']] ?? $data['title'], // 详情页模块标题(优先取关联主题标题)
  526. 'image' => set_file_url($data['detail_image']), // 详情页预览图
  527. 'update_time' => date('Y-m-d H:i:s', $data['detail_data_update_time']), // 详情页数据更新时间
  528. ],
  529. [
  530. 'key' => 'user',
  531. 'title' => $themeData[$data['user_data_id']] ?? $data['title'], // 用户中心模块标题(优先取关联主题标题)
  532. 'image' => set_file_url($data['user_image']), // 用户中心预览图
  533. 'update_time' => date('Y-m-d H:i:s', $data['user_data_update_time']), // 用户中心数据更新时间
  534. ],
  535. ];
  536. $theme['theme_data'] = json_decode($data['theme_data'], true); // 主题自身数据(JSON格式)
  537. return $theme;
  538. }
  539. /**
  540. * @description: 还原主题
  541. * @param int $id 主题ID
  542. * @return void
  543. */
  544. public function restoreTheme(int $id)
  545. {
  546. $data = $this->dao->get($id);
  547. if (!$data) throw new AdminException('主题不存在');
  548. $this->dao->update($id, [
  549. 'home_data' => $data['home_default_data'],
  550. 'home_data_id' => 0,
  551. 'home_image' => $data['home_default_image'],
  552. 'home_data_update_time' => time(),
  553. 'category_data' => $data['category_default_data'],
  554. 'category_data_id' => 0,
  555. 'category_image' => $data['category_default_image'],
  556. 'category_data_update_time' => time(),
  557. 'detail_data' => $data['detail_default_data'],
  558. 'detail_data_id' => 0,
  559. 'detail_image' => $data['detail_default_image'],
  560. 'detail_data_update_time' => time(),
  561. 'user_data' => $data['user_default_data'],
  562. 'user_data_id' => 0,
  563. 'user_image' => $data['user_default_image'],
  564. 'user_data_update_time' => time(),
  565. 'theme_data' => $data['theme_default_data'],
  566. 'theme_data_update_time' => time(),
  567. 'version' => uniqid(), // 更新版本号
  568. 'up_time' => time(), // 更新时间
  569. ]);
  570. return true;
  571. }
  572. /**
  573. * 删除主题
  574. *
  575. * 功能概述:
  576. * 软删除指定的主题(更新 is_del 字段)。
  577. *
  578. * @param int $id 主题ID
  579. * @return bool 操作成功返回 true
  580. * @throws AdminException 当主题不存在时抛出
  581. * @author wuhaotian
  582. * @email 442384644@qq.com
  583. * @date 2026/02/03
  584. */
  585. public function deleteTheme(int $id)
  586. {
  587. $data = $this->dao->get($id);
  588. if (!$data) throw new AdminException('主题不存在');
  589. if ($data['is_use']) throw new AdminException('当前主题正在使用中,不能删除');
  590. $this->dao->update($id, ['is_del' => 1]);
  591. return true;
  592. }
  593. /**
  594. * 获取当前启用主题的底部导航配置
  595. *
  596. * 功能概述:
  597. * 解析当前启用主题的首页数据,提取其中的底部导航(pagefoot)组件配置。
  598. *
  599. * @return array 返回名为 pagefoot 的组件配置数组,未找到时返回空数组
  600. * @throws ApiException 当启用主题不存在首页数据时抛出
  601. * @author wuhaotian
  602. * @email 442384644@qq.com
  603. * @date 2026/02/03
  604. */
  605. public function themeNavigation()
  606. {
  607. // 查询当前正在使用的主题的首页数据(JSON 字符串)
  608. $value = $this->dao->value(['is_use' => 1], 'home_data');
  609. if (!$value) {
  610. throw new ApiException('数据不存在');
  611. }
  612. // 初始化导航数据为空数组
  613. $navigation = [];
  614. // 若首页数据存在,则进行解析与遍历
  615. if ($value) {
  616. // 将 JSON 字符串解码为数组
  617. $value = json_decode($value, true);
  618. // 遍历首页组件,查找名称为 pagefoot 的底部导航组件
  619. foreach ($value['value'] as $item) {
  620. if (isset($item['name']) && strtolower($item['name']) === 'pagefoot') {
  621. // 找到后赋值并终止循环
  622. $navigation = $item;
  623. break;
  624. }
  625. }
  626. }
  627. // 返回导航配置(可能为空数组)
  628. return $navigation;
  629. }
  630. /**
  631. * 获取微页面列表
  632. *
  633. * 功能概述:
  634. * 分页查询微页面(page_type='micro')列表数据。
  635. *
  636. * 主要功能:
  637. * 1. 分页查询 - 根据系统分页参数获取数据
  638. * 2. 数据过滤 - 仅查询未删除且类型为微页面的记录
  639. * 3. 格式化 - 转换时间戳为可读日期格式
  640. *
  641. * @return array 包含列表数据 list 和总数 count 的数组
  642. * @throws \think\db\exception\DataNotFoundException
  643. * @throws \think\db\exception\DbException
  644. * @throws \think\db\exception\ModelNotFoundException
  645. * @author wuhaotian
  646. * @email 442384644@qq.com
  647. * @date 2026/02/03
  648. */
  649. public function getMicroPageList()
  650. {
  651. [$page, $limit] = $this->getPageValue(); // 获取分页参数
  652. $field = 'id,title,info,type,add_time,up_time,page_type'; // 查询字段
  653. $order = 'id desc'; // 排序
  654. $where = [
  655. 'is_del' => 0, // 未删除
  656. 'page_type' => 'micro', // 微页面类型
  657. ];
  658. $list = $this->dao->themeList($where, $field, $page, $limit, $order); // 查询列表
  659. foreach ($list as &$item) {
  660. // 格式化时间
  661. if (isset($item['add_time'])) $item['add_time'] = date('Y-m-d H:i', $item['add_time']);
  662. if (isset($item['up_time'])) $item['up_time'] = date('Y-m-d H:i', $item['up_time']);
  663. }
  664. $count = $this->dao->themeCount($where); // 获取总数
  665. return compact('list', 'count');
  666. }
  667. /**
  668. * 导出主题数据(核心逻辑)
  669. * 将主题配置及相关图片打包成 Zip 文件,返回下载地址
  670. *
  671. * @param $themeInfo
  672. * @return string 下载地址
  673. * @throws hinkdbexceptionDataNotFoundException
  674. * @throws hinkdbexceptionDbException
  675. * @throws hinkdbexceptionModelNotFoundException
  676. * @author wuhaotian
  677. * @email 442384644@qq.com
  678. * @date 2026/3/10
  679. */
  680. public function exportThemePackage($info): string
  681. {
  682. // 1. 设置导出临时目录
  683. $dir = public_path() . 'theme/download/' . $info['id'] . '/';
  684. // 2. 处理主要图片(首页图、分类图、详情图、个人中心图)
  685. $images = ['home_image', 'category_image', 'detail_image', 'user_image'];
  686. $defaultImages = ['home_default_image', 'category_default_image', 'detail_default_image', 'user_default_image'];
  687. $i = 1;
  688. foreach ($images as $key => $image) {
  689. if (isset($info[$image]) && $info[$image]) {
  690. $originalUrl = $info[$image];
  691. $isRemote = (bool)preg_match('/^https?:\/\//i', $originalUrl);
  692. if ($isRemote) {
  693. $urlPath = parse_url($originalUrl, PHP_URL_PATH) ?: $originalUrl;
  694. $extension = strtolower(pathinfo($urlPath, PATHINFO_EXTENSION)) ?: 'jpg';
  695. $newPath = $dir . $i . '_' . $image . '.' . $extension;
  696. $ch = curl_init();
  697. curl_setopt_array($ch, [
  698. CURLOPT_URL => $originalUrl,
  699. CURLOPT_RETURNTRANSFER => true,
  700. CURLOPT_FOLLOWLOCATION => true,
  701. CURLOPT_TIMEOUT => 30,
  702. CURLOPT_SSL_VERIFYPEER => false,
  703. CURLOPT_SSL_VERIFYHOST => false,
  704. CURLOPT_REFERER => rtrim(sys_config('site_url'), '/'),
  705. CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; CRMEB/1.0)',
  706. CURLOPT_HTTPHEADER => ['Accept: image/webp,image/*,*/*'],
  707. ]);
  708. $content = curl_exec($ch);
  709. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  710. curl_close($ch);
  711. if ($content && $httpCode === 200 && file_put_contents($newPath, $content) !== false) {
  712. $info[$image] = 'theme/download/' . $i . '_' . $image . '.' . $extension;
  713. $info[$defaultImages[$key]] = 'theme/download/' . $i . '_' . $image . '.' . $extension;
  714. }
  715. } else {
  716. $localPath = public_path() . ltrim(preg_replace('/^https?:\/\/[^\/]+/', '', $originalUrl), '/');
  717. if (!file_exists($localPath)) {
  718. $i++;
  719. continue;
  720. }
  721. $extension = strtolower(pathinfo($localPath, PATHINFO_EXTENSION)) ?: 'jpg';
  722. $newPath = $dir . $i . '_' . $image . '.' . $extension;
  723. if (copy($localPath, $newPath)) {
  724. $info[$image] = 'theme/download/' . $i . '_' . $image . '.' . $extension;
  725. $info[$defaultImages[$key]] = 'theme/download/' . $i . '_' . $image . '.' . $extension;
  726. }
  727. }
  728. }
  729. $i++;
  730. }
  731. // 3. 处理 home_data / detail_data / user_data 中的图片
  732. $imagesDir = $dir . 'images/';
  733. $index = 1;
  734. $map = [];
  735. $isAttachImage = function ($str) {
  736. if (!is_string($str) || $str === '') return false;
  737. if (strpos($str, 'uploads/attach') !== false) return true;
  738. if (preg_match('/^https?:\/\//i', $str)) return true;
  739. return false;
  740. };
  741. $process = function (&$value, $key) use (&$index, &$map, $imagesDir, $isAttachImage) {
  742. if (!is_string($value) || !$isAttachImage($value)) return;
  743. if (!isset($map[$value])) {
  744. $path = parse_url($value, PHP_URL_PATH);
  745. $path = $path ?: $value;
  746. $basename = basename($path);
  747. $ext = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
  748. if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'])) {
  749. $ext = 'jpg';
  750. $basename = md5($value) . '.' . $ext;
  751. }
  752. $dest = $imagesDir . $basename;
  753. $rel = 'images/' . $basename;
  754. $ok = false;
  755. $isRemote = preg_match('/^https?:\/\//i', $value);
  756. if ($isRemote) {
  757. $ch = curl_init();
  758. curl_setopt_array($ch, [
  759. CURLOPT_URL => $value,
  760. CURLOPT_RETURNTRANSFER => true,
  761. CURLOPT_FOLLOWLOCATION => true,
  762. CURLOPT_TIMEOUT => 30,
  763. CURLOPT_SSL_VERIFYPEER => false,
  764. CURLOPT_SSL_VERIFYHOST => false,
  765. CURLOPT_REFERER => rtrim(sys_config('site_url'), '/'),
  766. CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; CRMEB/1.0)',
  767. CURLOPT_HTTPHEADER => ['Accept: image/webp,image/*,*/*'],
  768. ]);
  769. $content = curl_exec($ch);
  770. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  771. curl_close($ch);
  772. if ($content && $httpCode === 200) {
  773. $ok = (file_put_contents($dest, $content) !== false);
  774. }
  775. } else {
  776. $src = public_path() . ltrim($path, '/');
  777. if (file_exists($src)) $ok = @copy($src, $dest);
  778. }
  779. if ($ok) {
  780. $map[$value] = $rel;
  781. $index++;
  782. } else {
  783. return;
  784. }
  785. }
  786. $value = $map[$value] ?? $value;
  787. };
  788. $homeData = json_decode($info['home_data'] ?? '[]', true);
  789. $detailData = json_decode($info['detail_data'] ?? '[]', true);
  790. $userData = json_decode($info['user_data'] ?? '[]', true);
  791. if (is_array($homeData)) array_walk_recursive($homeData, $process);
  792. if (is_array($detailData)) array_walk_recursive($detailData, $process);
  793. if (is_array($userData)) array_walk_recursive($userData, $process);
  794. $info['home_data'] = json_encode($homeData, JSON_UNESCAPED_UNICODE);
  795. $info['detail_data'] = json_encode($detailData, JSON_UNESCAPED_UNICODE);
  796. $info['user_data'] = json_encode($userData, JSON_UNESCAPED_UNICODE);
  797. $info['home_default_data'] = json_encode($homeData, JSON_UNESCAPED_UNICODE);
  798. $info['detail_default_data'] = json_encode($detailData, JSON_UNESCAPED_UNICODE);
  799. $info['user_default_data'] = json_encode($userData, JSON_UNESCAPED_UNICODE);
  800. $info['theme_data'] = $info['theme_default_data'] = json_decode($info['theme_data'], true);
  801. // 4. 写入 config.json
  802. file_put_contents($dir . 'config.json', json_encode($info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
  803. // 7. 打包成 zip
  804. $zip = new \ZipArchive();
  805. $zip->open($dir . $info['title'] . '.zip', \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
  806. $rootPath = realpath($dir);
  807. $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS));
  808. foreach ($files as $file) {
  809. if ($file->isDir()) continue;
  810. $filePath = $file->getRealPath();
  811. if (basename($filePath) === $info['title'] . '.zip') continue;
  812. $relativePath = ltrim(str_replace($rootPath, '', $filePath), DIRECTORY_SEPARATOR);
  813. $zip->addFile($filePath, $relativePath);
  814. }
  815. $zip->close();
  816. return sys_config('site_url') . '/theme/download/' . $info['id'] . '/' . $info['title'] . '.zip';
  817. }
  818. }