InstallController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <?php
  2. namespace plugin\saipackage\app\controller;
  3. use plugin\saiadmin\app\cache\UserMenuCache;
  4. use plugin\saiadmin\app\middleware\SystemLog;
  5. use plugin\saiadmin\app\middleware\CheckLogin;
  6. use plugin\saiadmin\basic\BaseController;
  7. use plugin\saiadmin\exception\ApiException;
  8. use plugin\saipackage\app\logic\InstallLogic;
  9. use Saithink\Saipackage\service\Server;
  10. use Saithink\Saipackage\service\Version;
  11. use support\annotation\Middleware;
  12. use support\Request;
  13. use support\Response;
  14. use Throwable;
  15. #[Middleware(CheckLogin::class, SystemLog::class)]
  16. class InstallController extends BaseController
  17. {
  18. /**
  19. * 构造
  20. */
  21. public function __construct()
  22. {
  23. parent::__construct();
  24. if ($this->adminId > 1) {
  25. throw new ApiException('仅超级管理员能够操作');
  26. }
  27. }
  28. /**
  29. * 环境检查状态
  30. */
  31. static string $ok = 'ok';
  32. static string $fail = 'fail';
  33. static string $warn = 'warn';
  34. static array $needDependentVersion = [
  35. 'php' => '8.1.0',
  36. 'saiadmin' => '6.0.0',
  37. 'saipackage' => '6.0.0',
  38. ];
  39. /**
  40. * 应用列表
  41. * @param Request $request
  42. * @return Response
  43. */
  44. public function index(Request $request): Response
  45. {
  46. $data = Server::installedList(runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR);
  47. $phpVersion = phpversion();
  48. $phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
  49. $phpVersionNotes = '正常';
  50. if (!$phpVersionCompare) {
  51. $phpVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['php'];
  52. }
  53. $saiadminVersion = config('plugin.saiadmin.app.version');
  54. $saiadminVersionCompare = Version::compare(self::$needDependentVersion['saiadmin'], $saiadminVersion);
  55. $saiadminVersionNotes = '正常';
  56. if (!$saiadminVersionCompare) {
  57. $saiadminVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saiadmin'];
  58. }
  59. $saithinkVersion = config('plugin.saipackage.app.version');
  60. $saithinkVersionCompare = Version::compare(self::$needDependentVersion['saipackage'], $saithinkVersion);
  61. $saithinkVersionNotes = '正常';
  62. if (!$saithinkVersionCompare) {
  63. $saithinkVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saipackage'];
  64. }
  65. return $this->success([
  66. 'version' => [
  67. 'php_version' => [
  68. 'describe' => $phpVersion,
  69. 'state' => $phpVersionCompare ? self::$ok : self::$fail,
  70. 'notes' => $phpVersionNotes,
  71. ],
  72. 'saiadmin_version' => [
  73. 'describe' => $saiadminVersion,
  74. 'state' => $saiadminVersionCompare ? self::$ok : self::$fail,
  75. 'notes' => $saiadminVersionNotes,
  76. ],
  77. 'saipackage_version' => [
  78. 'describe' => $saithinkVersion,
  79. 'state' => $saithinkVersionCompare ? self::$ok : self::$fail,
  80. 'notes' => $saithinkVersionNotes,
  81. ],
  82. ],
  83. 'data' => $data
  84. ]);
  85. }
  86. /**
  87. * 上传插件
  88. * @param Request $request
  89. * @return Response
  90. * @throws Throwable
  91. */
  92. public function upload(Request $request): Response
  93. {
  94. $spl_file = current($request->file());
  95. if (!$spl_file->isValid()) {
  96. return $this->fail('上传文件校验失败');
  97. }
  98. $config = config('plugin.saipackage.upload', [
  99. 'size' => 1024 * 1024 * 5,
  100. 'type' => ['zip']
  101. ]);
  102. if (!in_array($spl_file->getUploadExtension(), $config['type'])) {
  103. return $this->fail('文件格式上传失败,请选择zip格式文件上传');
  104. }
  105. if ($spl_file->getSize() > $config['size']) {
  106. return $this->fail('文件大小不能超过5M');
  107. }
  108. $install = new InstallLogic();
  109. $info = $install->upload($spl_file);
  110. return $this->success($info);
  111. }
  112. /**
  113. * 安装插件
  114. * @param Request $request
  115. * @return Response
  116. * @throws Throwable
  117. */
  118. public function install(Request $request): Response
  119. {
  120. $appName = $request->post("appName", '');
  121. if (empty($appName)) {
  122. return $this->fail('参数错误');
  123. }
  124. $install = new InstallLogic($appName);
  125. $info = $install->install();
  126. UserMenuCache::clearMenuCache();
  127. return $this->success($info);
  128. }
  129. /**
  130. * 卸载插件
  131. * @param Request $request
  132. * @return Response
  133. * @throws Throwable
  134. */
  135. public function uninstall(Request $request): Response
  136. {
  137. $appName = $request->post("appName", '');
  138. if (empty($appName)) {
  139. return $this->fail('参数错误');
  140. }
  141. $install = new InstallLogic($appName);
  142. $install->uninstall();
  143. UserMenuCache::clearMenuCache();
  144. return $this->success('卸载插件成功');
  145. }
  146. /**
  147. * 重启
  148. * @param Request $request
  149. * @return Response
  150. */
  151. public function reload(Request $request): Response
  152. {
  153. Server::restart();
  154. return $this->success('重载成功');
  155. }
  156. // ========== 商店代理接口 ==========
  157. /**
  158. * 代理请求封装
  159. */
  160. protected function proxyRequest(string $url, string $method = 'GET', ?string $token = null, ?array $postData = null, int $timeout = 10): array
  161. {
  162. $headers = [];
  163. if ($token) {
  164. $headers[] = "Authorization: Bearer {$token}";
  165. }
  166. if ($postData !== null) {
  167. $headers[] = "Content-Type: application/json";
  168. }
  169. $context = stream_context_create([
  170. 'http' => [
  171. 'method' => $method,
  172. 'header' => implode("\r\n", $headers),
  173. 'content' => $postData ? json_encode($postData) : null,
  174. 'timeout' => $timeout,
  175. ],
  176. 'ssl' => [
  177. 'verify_peer' => false,
  178. 'verify_peer_name' => false,
  179. ],
  180. ]);
  181. $response = file_get_contents($url, false, $context);
  182. if ($response === false) {
  183. return ['success' => false, 'message' => '请求失败'];
  184. }
  185. // 尝试解析 JSON
  186. $data = json_decode($response, true);
  187. if ($data && isset($data['code'])) {
  188. if ($data['code'] === 200) {
  189. return ['success' => true, 'data' => $data['data'] ?? null];
  190. }
  191. return ['success' => false, 'message' => $data['message'] ?? '请求失败'];
  192. }
  193. // 非 JSON 响应(可能是文件)
  194. return ['success' => true, 'raw' => $response, 'headers' => $http_response_header ?? []];
  195. }
  196. /**
  197. * 获取应用商店列表
  198. */
  199. public function appList(Request $request): Response
  200. {
  201. $params = http_build_query([
  202. 'page' => $request->input('page', 1),
  203. 'limit' => $request->input('limit', 16),
  204. 'price' => $request->input('price', 'all'),
  205. 'type' => $request->input('type', ''),
  206. 'keywords' => $request->input('keywords', ''),
  207. ]);
  208. $result = $this->proxyRequest("https://saas.saithink.top/dev-api/app/saistore/api/store/appList?{$params}");
  209. return $result['success']
  210. ? $this->success($result['data'])
  211. : $this->fail($result['message']);
  212. }
  213. /**
  214. * 获取商店验证码
  215. */
  216. public function storeCaptcha(): Response
  217. {
  218. $result = $this->proxyRequest("https://saas.saithink.top/dev-api/app/saiuser/api/common/index/captcha");
  219. return $result['success']
  220. ? $this->success($result['data'])
  221. : $this->fail($result['message']);
  222. }
  223. /**
  224. * 商店登录
  225. */
  226. public function storeLogin(Request $request): Response
  227. {
  228. $result = $this->proxyRequest(
  229. "https://saas.saithink.top/dev-api/app/saiuser/api/common/index/accountLogin",
  230. 'POST',
  231. null,
  232. [
  233. 'username' => $request->input('username'),
  234. 'password' => $request->input('password'),
  235. 'code' => $request->input('code'),
  236. 'uuid' => $request->input('uuid'),
  237. ]
  238. );
  239. return $result['success']
  240. ? $this->success($result['data'])
  241. : $this->fail($result['message']);
  242. }
  243. /**
  244. * 获取商店用户信息
  245. */
  246. public function storeUserInfo(Request $request): Response
  247. {
  248. $token = $request->input('token');
  249. if (empty($token)) {
  250. return $this->fail('未登录');
  251. }
  252. $result = $this->proxyRequest(
  253. "https://saas.saithink.top/dev-api/app/saiuser/api/user/user/userInfo",
  254. 'GET',
  255. $token
  256. );
  257. return $result['success']
  258. ? $this->success($result['data'])
  259. : $this->fail($result['message']);
  260. }
  261. /**
  262. * 获取已购应用列表
  263. */
  264. public function storePurchasedApps(Request $request): Response
  265. {
  266. $token = $request->input('token');
  267. if (empty($token)) {
  268. return $this->fail('未登录');
  269. }
  270. $result = $this->proxyRequest(
  271. "https://saas.saithink.top/dev-api/app/saistore/api/StoreOrder/orderList?saiType=all",
  272. 'GET',
  273. $token
  274. );
  275. return $result['success']
  276. ? $this->success($result['data'])
  277. : $this->fail($result['message']);
  278. }
  279. /**
  280. * 获取应用版本列表
  281. */
  282. public function storeAppVersions(Request $request): Response
  283. {
  284. $token = $request->input('token');
  285. $appId = $request->input('app_id');
  286. if (empty($token)) {
  287. return $this->fail('未登录');
  288. }
  289. $result = $this->proxyRequest(
  290. "https://saas.saithink.top/dev-api/app/saistore/api/StoreOrder/appVersionList?app_id={$appId}",
  291. 'GET',
  292. $token
  293. );
  294. return $result['success']
  295. ? $this->success($result['data'])
  296. : $this->fail($result['message']);
  297. }
  298. /**
  299. * 下载应用 - 下载并调用 InstallLogic 处理
  300. */
  301. public function storeDownloadApp(Request $request): Response
  302. {
  303. $token = $request->input('token');
  304. $versionId = $request->input('id');
  305. if (empty($token)) {
  306. return $this->fail('未登录');
  307. }
  308. if (empty($versionId)) {
  309. return $this->fail('版本ID不能为空');
  310. }
  311. $result = $this->proxyRequest(
  312. "https://saas.saithink.top/dev-api/app/saistore/api/StoreOrder/downloadVersion",
  313. 'POST',
  314. $token,
  315. ['version_id' => (int) $versionId],
  316. 60
  317. );
  318. if (!$result['success']) {
  319. return $this->fail($result['message'] ?? '下载失败');
  320. }
  321. if (!isset($result['raw'])) {
  322. return $this->fail('下载失败');
  323. }
  324. // 保存临时 zip 文件
  325. $tempZip = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR . 'downloadTemp' . date('YmdHis') . '.zip';
  326. if (!is_dir(dirname($tempZip))) {
  327. mkdir(dirname($tempZip), 0755, true);
  328. }
  329. file_put_contents($tempZip, $result['raw']);
  330. try {
  331. // 调用 InstallLogic 处理
  332. $install = new InstallLogic();
  333. $info = $install->uploadFromPath($tempZip);
  334. return $this->success($info, '下载成功,请在插件列表中安装');
  335. } catch (Throwable $e) {
  336. @unlink($tempZip);
  337. return $this->fail($e->getMessage());
  338. }
  339. }
  340. }