OpenApiService.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. <?php
  2. namespace App\Module\OpenAPI\Services;
  3. use App\Module\OpenAPI\Models\OpenApiApp;
  4. use App\Module\OpenAPI\Enums\APP_STATUS;
  5. use App\Module\OpenAPI\Enums\AUTH_TYPE;
  6. use App\Module\OpenAPI\Enums\SCOPE_TYPE;
  7. use App\Module\OpenAPI\Events\AppCreatedEvent;
  8. use Illuminate\Support\Facades\Cache;
  9. use Illuminate\Support\Facades\Event;
  10. use Illuminate\Support\Facades\Hash;
  11. /**
  12. * OpenAPI核心服务
  13. */
  14. class OpenApiService
  15. {
  16. /**
  17. * 创建新应用
  18. *
  19. * @param array $data
  20. * @return OpenApiApp
  21. */
  22. public function createApp(array $data): OpenApiApp
  23. {
  24. // 验证必填字段
  25. $this->validateRequiredFields($data);
  26. // 生成应用ID和密钥
  27. $data['app_id'] = OpenApiApp::generateAppId();
  28. $data['app_secret'] = OpenApiApp::generateAppSecret();
  29. // 设置默认值
  30. $data['status'] = $data['status'] ?? config('openapi.app.default_status', APP_STATUS::PENDING->value);
  31. $data['auth_type'] = $data['auth_type'] ?? config('openapi.auth.default_type', AUTH_TYPE::API_KEY->value);
  32. $data['scopes'] = $data['scopes'] ?? config('openapi.scopes.default', []);
  33. // 设置过期时间
  34. if (!isset($data['expires_at'])) {
  35. $expireDays = config('openapi.app.expire_days', 365);
  36. $data['expires_at'] = now()->addDays($expireDays);
  37. }
  38. // 加密应用密钥
  39. if (config('openapi.security.encrypt_secrets', true)) {
  40. $data['app_secret'] = encrypt($data['app_secret']);
  41. }
  42. // 创建应用
  43. $app = OpenApiApp::create($data);
  44. // 触发应用创建事件
  45. Event::dispatch(new AppCreatedEvent($app));
  46. return $app;
  47. }
  48. /**
  49. * 验证必填字段
  50. *
  51. * @param array $data
  52. * @throws \InvalidArgumentException
  53. */
  54. protected function validateRequiredFields(array $data): void
  55. {
  56. $requiredFields = config('openapi.app.required_fields', ['name', 'description', 'callback_url']);
  57. foreach ($requiredFields as $field) {
  58. if (empty($data[$field])) {
  59. throw new \InvalidArgumentException("字段 {$field} 是必填的");
  60. }
  61. }
  62. }
  63. /**
  64. * 审核应用
  65. *
  66. * @param int $appId
  67. * @param bool $approved
  68. * @param string $note
  69. * @param int $approvedBy
  70. * @return OpenApiApp
  71. */
  72. public function approveApp(int $appId, bool $approved, string $note = '', int $approvedBy = null): OpenApiApp
  73. {
  74. $app = OpenApiApp::findOrFail($appId);
  75. if ($app->status !== APP_STATUS::PENDING->value) {
  76. throw new \InvalidArgumentException('只能审核待审核状态的应用');
  77. }
  78. $app->update([
  79. 'status' => $approved ? APP_STATUS::APPROVED->value : APP_STATUS::REJECTED->value,
  80. 'approved_at' => now(),
  81. 'approved_by' => $approvedBy,
  82. 'approved_note' => $note,
  83. ]);
  84. // 清除缓存
  85. $this->clearAppCache($app->app_id);
  86. return $app;
  87. }
  88. /**
  89. * 激活应用
  90. *
  91. * @param int $appId
  92. * @return OpenApiApp
  93. */
  94. public function activateApp(int $appId): OpenApiApp
  95. {
  96. $app = OpenApiApp::findOrFail($appId);
  97. if (!in_array($app->status, [APP_STATUS::APPROVED->value, APP_STATUS::SUSPENDED->value])) {
  98. throw new \InvalidArgumentException('只能激活已审核或已暂停的应用');
  99. }
  100. $app->update(['status' => APP_STATUS::ACTIVE->value]);
  101. // 清除缓存
  102. $this->clearAppCache($app->app_id);
  103. return $app;
  104. }
  105. /**
  106. * 暂停应用
  107. *
  108. * @param int $appId
  109. * @param string $reason
  110. * @return OpenApiApp
  111. */
  112. public function suspendApp(int $appId, string $reason = ''): OpenApiApp
  113. {
  114. $app = OpenApiApp::findOrFail($appId);
  115. if ($app->status !== APP_STATUS::ACTIVE->value) {
  116. throw new \InvalidArgumentException('只能暂停激活状态的应用');
  117. }
  118. $app->update([
  119. 'status' => APP_STATUS::SUSPENDED->value,
  120. 'approved_note' => $reason,
  121. ]);
  122. // 清除缓存
  123. $this->clearAppCache($app->app_id);
  124. return $app;
  125. }
  126. /**
  127. * 禁用应用
  128. *
  129. * @param int $appId
  130. * @param string $reason
  131. * @return OpenApiApp
  132. */
  133. public function disableApp(int $appId, string $reason = ''): OpenApiApp
  134. {
  135. $app = OpenApiApp::findOrFail($appId);
  136. $app->update([
  137. 'status' => APP_STATUS::DISABLED->value,
  138. 'approved_note' => $reason,
  139. ]);
  140. // 清除缓存
  141. $this->clearAppCache($app->app_id);
  142. return $app;
  143. }
  144. /**
  145. * 重新生成应用密钥
  146. *
  147. * @param int $appId
  148. * @return OpenApiApp
  149. */
  150. public function regenerateSecret(int $appId): OpenApiApp
  151. {
  152. $app = OpenApiApp::findOrFail($appId);
  153. if (!config('openapi.api_key.allow_regenerate', true)) {
  154. throw new \InvalidArgumentException('不允许重新生成密钥');
  155. }
  156. $newSecret = OpenApiApp::generateAppSecret();
  157. // 加密应用密钥
  158. if (config('openapi.security.encrypt_secrets', true)) {
  159. $newSecret = encrypt($newSecret);
  160. }
  161. $app->update(['app_secret' => $newSecret]);
  162. // 清除缓存
  163. $this->clearAppCache($app->app_id);
  164. return $app;
  165. }
  166. /**
  167. * 更新应用权限
  168. *
  169. * @param int $appId
  170. * @param array $scopes
  171. * @return OpenApiApp
  172. */
  173. public function updateScopes(int $appId, array $scopes): OpenApiApp
  174. {
  175. $app = OpenApiApp::findOrFail($appId);
  176. // 验证权限范围
  177. $this->validateScopes($scopes);
  178. $app->update(['scopes' => $scopes]);
  179. // 清除缓存
  180. $this->clearAppCache($app->app_id);
  181. return $app;
  182. }
  183. /**
  184. * 验证权限范围
  185. *
  186. * @param array $scopes
  187. * @throws \InvalidArgumentException
  188. */
  189. protected function validateScopes(array $scopes): void
  190. {
  191. $validScopes = array_column(SCOPE_TYPE::cases(), 'value');
  192. $validScopes[] = '*'; // 允许通配符
  193. foreach ($scopes as $scope) {
  194. if (!in_array($scope, $validScopes)) {
  195. throw new \InvalidArgumentException("无效的权限范围: {$scope}");
  196. }
  197. }
  198. }
  199. /**
  200. * 通过应用ID获取应用信息
  201. *
  202. * @param string $appId
  203. * @return OpenApiApp|null
  204. */
  205. public function getAppByAppId(string $appId): ?OpenApiApp
  206. {
  207. $cacheKey = config('openapi.cache.keys.app_info');
  208. $cacheKey = str_replace('{app_id}', $appId, $cacheKey);
  209. return Cache::remember($cacheKey, config('openapi.cache.default_ttl', 300), function () use ($appId) {
  210. return OpenApiApp::where('app_id', $appId)->first();
  211. });
  212. }
  213. /**
  214. * 验证应用认证
  215. *
  216. * @param string $appId
  217. * @param string $appSecret
  218. * @return OpenApiApp|null
  219. */
  220. public function validateApp(string $appId, string $appSecret): ?OpenApiApp
  221. {
  222. $app = $this->getAppByAppId($appId);
  223. if (!$app) {
  224. return null;
  225. }
  226. // 检查应用状态
  227. if (!$app->can_call_api) {
  228. return null;
  229. }
  230. // 检查应用是否过期
  231. if ($app->is_expired) {
  232. return null;
  233. }
  234. // 验证密钥
  235. $storedSecret = $app->app_secret;
  236. if (config('openapi.security.encrypt_secrets', true)) {
  237. $storedSecret = decrypt($storedSecret);
  238. }
  239. if (!hash_equals($storedSecret, $appSecret)) {
  240. return null;
  241. }
  242. return $app;
  243. }
  244. /**
  245. * 检查应用权限
  246. *
  247. * @param OpenApiApp $app
  248. * @param string $scope
  249. * @return bool
  250. */
  251. public function checkScope(OpenApiApp $app, string $scope): bool
  252. {
  253. return $app->hasScope($scope);
  254. }
  255. /**
  256. * 检查IP白名单
  257. *
  258. * @param OpenApiApp $app
  259. * @param string $ip
  260. * @return bool
  261. */
  262. public function checkIpWhitelist(OpenApiApp $app, string $ip): bool
  263. {
  264. return $app->isIpAllowed($ip);
  265. }
  266. /**
  267. * 更新应用最后使用时间
  268. *
  269. * @param OpenApiApp $app
  270. * @return void
  271. */
  272. public function updateLastUsed(OpenApiApp $app): void
  273. {
  274. $app->updateLastUsed();
  275. }
  276. /**
  277. * 获取应用统计信息
  278. *
  279. * @param string $appId
  280. * @param string $period
  281. * @return array
  282. */
  283. public function getAppStats(string $appId, string $period = 'day'): array
  284. {
  285. $startTime = $this->getStatsPeriodStart($period);
  286. // 获取基础统计
  287. $stats = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId)
  288. ->where('created_at', '>=', $startTime)
  289. ->selectRaw('
  290. SUM(request_count) as total_requests,
  291. SUM(success_count) as total_success,
  292. SUM(error_count) as total_errors,
  293. AVG(avg_response_time) as avg_response_time,
  294. MAX(max_response_time) as max_response_time,
  295. MIN(min_response_time) as min_response_time,
  296. SUM(rate_limit_hits) as total_rate_limit_hits,
  297. SUM(unique_ips) as total_unique_ips
  298. ')
  299. ->first();
  300. $successRate = $stats->total_requests > 0
  301. ? round(($stats->total_success / $stats->total_requests) * 100, 2)
  302. : 0;
  303. $errorRate = $stats->total_requests > 0
  304. ? round(($stats->total_errors / $stats->total_requests) * 100, 2)
  305. : 0;
  306. return [
  307. 'period' => $period,
  308. 'start_time' => $startTime,
  309. 'total_requests' => $stats->total_requests ?? 0,
  310. 'total_success' => $stats->total_success ?? 0,
  311. 'total_errors' => $stats->total_errors ?? 0,
  312. 'success_rate' => $successRate,
  313. 'error_rate' => $errorRate,
  314. 'avg_response_time' => round($stats->avg_response_time ?? 0, 2),
  315. 'max_response_time' => $stats->max_response_time ?? 0,
  316. 'min_response_time' => $stats->min_response_time ?? 0,
  317. 'total_rate_limit_hits' => $stats->total_rate_limit_hits ?? 0,
  318. 'total_unique_ips' => $stats->total_unique_ips ?? 0,
  319. ];
  320. }
  321. /**
  322. * 清除应用缓存
  323. *
  324. * @param string $appId
  325. * @return void
  326. */
  327. protected function clearAppCache(string $appId): void
  328. {
  329. $cacheKey = config('openapi.cache.keys.app_info');
  330. $cacheKey = str_replace('{app_id}', $appId, $cacheKey);
  331. Cache::forget($cacheKey);
  332. }
  333. /**
  334. * 批量处理过期应用
  335. *
  336. * @return int
  337. */
  338. public function handleExpiredApps(): int
  339. {
  340. $expiredApps = OpenApiApp::where('expires_at', '<', now())
  341. ->where('status', '!=', APP_STATUS::EXPIRED->value)
  342. ->get();
  343. $count = 0;
  344. foreach ($expiredApps as $app) {
  345. $app->update(['status' => APP_STATUS::EXPIRED->value]);
  346. $this->clearAppCache($app->app_id);
  347. $count++;
  348. }
  349. return $count;
  350. }
  351. /**
  352. * 获取用户的应用列表
  353. *
  354. * @param int $userId
  355. * @return \Illuminate\Database\Eloquent\Collection
  356. */
  357. public function getUserApps(int $userId)
  358. {
  359. return OpenApiApp::byUser($userId)->orderBy('created_at', 'desc')->get();
  360. }
  361. /**
  362. * 检查用户是否可以创建更多应用
  363. *
  364. * @param int $userId
  365. * @return bool
  366. */
  367. public function canCreateMoreApps(int $userId): bool
  368. {
  369. $maxApps = config('openapi.app.max_apps_per_user', 10);
  370. $currentCount = OpenApiApp::byUser($userId)->count();
  371. return $currentCount < $maxApps;
  372. }
  373. /**
  374. * 更新应用信息
  375. *
  376. * @param int $appId
  377. * @param array $data
  378. * @return OpenApiApp
  379. */
  380. public function updateApp(int $appId, array $data): OpenApiApp
  381. {
  382. $app = OpenApiApp::findOrFail($appId);
  383. // 过滤允许更新的字段
  384. $allowedFields = ['name', 'description', 'website', 'logo', 'callback_url', 'contact_email'];
  385. $updateData = array_intersect_key($data, array_flip($allowedFields));
  386. $app->update($updateData);
  387. // 清除缓存
  388. $this->clearAppCache($app->app_id);
  389. return $app->fresh();
  390. }
  391. /**
  392. * 重新生成应用密钥
  393. *
  394. * @param int $appId
  395. * @return string
  396. */
  397. public function regenerateAppSecret(int $appId): string
  398. {
  399. $app = OpenApiApp::findOrFail($appId);
  400. $newSecret = OpenApiApp::generateAppSecret();
  401. $app->update(['app_secret' => $newSecret]);
  402. // 清除缓存
  403. $this->clearAppCache($app->app_id);
  404. return $newSecret;
  405. }
  406. /**
  407. * 获取应用配额信息
  408. *
  409. * @param string $appId
  410. * @return array
  411. */
  412. public function getAppQuota(string $appId): array
  413. {
  414. $app = OpenApiApp::where('app_id', $appId)->firstOrFail();
  415. // 获取今日使用量
  416. $todayUsage = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId)
  417. ->where('date', now()->toDateString())
  418. ->sum('request_count');
  419. // 获取本月使用量
  420. $monthUsage = \App\Module\OpenAPI\Models\OpenApiStats::where('app_id', $appId)
  421. ->whereBetween('date', [
  422. now()->startOfMonth()->toDateString(),
  423. now()->endOfMonth()->toDateString()
  424. ])
  425. ->sum('request_count');
  426. // 获取配额限制
  427. $rateLimits = $app->rate_limits ?? [];
  428. $dailyLimit = $rateLimits['requests_per_day'] ?? 0;
  429. $monthlyLimit = $rateLimits['requests_per_month'] ?? 0;
  430. return [
  431. 'today' => [
  432. 'used' => $todayUsage,
  433. 'limit' => $dailyLimit,
  434. 'remaining' => max(0, $dailyLimit - $todayUsage),
  435. 'percentage' => $dailyLimit > 0 ? round(($todayUsage / $dailyLimit) * 100, 2) : 0,
  436. ],
  437. 'month' => [
  438. 'used' => $monthUsage,
  439. 'limit' => $monthlyLimit,
  440. 'remaining' => max(0, $monthlyLimit - $monthUsage),
  441. 'percentage' => $monthlyLimit > 0 ? round(($monthUsage / $monthlyLimit) * 100, 2) : 0,
  442. ],
  443. ];
  444. }
  445. /**
  446. * 获取统计周期的开始时间
  447. *
  448. * @param string $period
  449. * @return \Carbon\Carbon
  450. */
  451. protected function getStatsPeriodStart(string $period): \Carbon\Carbon
  452. {
  453. return match ($period) {
  454. 'hour' => now()->subHour(),
  455. 'day' => now()->subDay(),
  456. 'week' => now()->subWeek(),
  457. 'month' => now()->subMonth(),
  458. default => now()->subDay(),
  459. };
  460. }
  461. }