run(fn () => AppSetting::query()->first() ?: [ 'id' => null, 'ios_frontend_visible' => 0, 'android_frontend_visible' => 0, ]); } public function saveDisplay() { return $this->run(function () { $params = request()->validate([ 'ios_frontend_visible' => ['required', Rule::in([0, 1])], 'android_frontend_visible' => ['required', Rule::in([0, 1])], ]); return DB::transaction(function () use ($params) { $settingId = AppSetting::query()->value('id'); $setting = $settingId ? AppSetting::query()->lockForUpdate()->findOrFail($settingId) : AppSetting::query()->create([]); $before = $setting->toArray(); $setting->fill($params + OperationAuditService::actor())->save(); OperationAuditService::record('app_display_setting', (int)$setting->id, 'update', $before, $setting); return $setting->fresh(); }); }); } public function packages() { return $this->run(function () { $params = request()->validate($this->listRules([ 'platform' => ['nullable', Rule::in(['android', 'ios'])], 'status' => ['nullable', Rule::in([0, 1])], ])); $query = AppPackage::query(); foreach (['platform', 'status'] as $field) { if (array_key_exists($field, $params) && $params[$field] !== null && $params[$field] !== '') { $query->where($field, $params[$field]); } } return $this->paginate($query->orderByDesc('id'), $params); }); } public function savePackage() { return $this->run(function () { $params = request()->validate([ 'id' => ['nullable', 'integer'], 'platform' => ['required', Rule::in(['android', 'ios'])], 'version' => ['required', 'string', 'max:50'], 'package_name' => ['required', 'string', 'max:150'], 'force_update' => ['required', Rule::in([0, 1])], 'package_type' => ['required', Rule::in(['android_apk', 'testflight', 'app_store', 'enterprise', 'web'])], 'download_url' => ['required', 'url', 'max:1000'], 'update_content' => ['nullable', 'string', 'max:10000'], 'status' => ['nullable', Rule::in([0, 1])], ]); $allowedTypes = $params['platform'] === 'android' ? ['android_apk', 'web'] : ['testflight', 'app_store', 'enterprise', 'web']; if (!in_array($params['package_type'], $allowedTypes, true)) { throw ValidationException::withMessages(['package_type' => '安装包类型与平台不匹配']); } return $this->saveModel(AppPackage::class, $params, 'app_package'); }); } public function packageStatus() { return $this->setStatus(AppPackage::class, 'app_package'); } public function deletePackage() { return $this->deleteModel(AppPackage::class, 'app_package'); } public function downloadStats() { return $this->run(function () { $params = request()->validate([ 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1', 'max:200'], 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'], ]); $page = (int)($params['page'] ?? 1); $limit = (int)($params['limit'] ?? 20); $endDate = $params['end_date'] ?? date('Y-m-d'); $startDate = $params['start_date'] ?? date('Y-m-d', strtotime($endDate . ' -30 days')); if (strtotime($startDate) > strtotime($endDate)) { throw ValidationException::withMessages(['end_date' => '结束日期不能早于开始日期']); } if (strtotime($endDate) - strtotime($startDate) > 366 * 86400) { throw ValidationException::withMessages(['end_date' => '单次查询日期范围不能超过366天']); } $base = AppDownloadEvent::query()->whereBetween('occurred_at', [ $startDate . ' 00:00:00', $endDate . ' 23:59:59', ]); $clickQuery = (clone $base)->where('event_type', 'click') ->select(['platform', 'source', 'download_url']) ->selectRaw('COUNT(*) AS click_count') ->groupBy(['platform', 'source', 'download_url']); $clickTotal = DB::query()->fromSub(clone $clickQuery, 'click_stats')->count(); $clicks = $clickQuery->orderBy('platform')->orderBy('source') ->forPage($page, $limit)->get(); $openQuery = (clone $base)->where('event_type', 'open') ->select(['platform', 'package_type']) ->selectRaw('COUNT(*) AS open_count') ->groupBy(['platform', 'package_type']); $openTotal = DB::query()->fromSub(clone $openQuery, 'open_stats')->count(); $opens = $openQuery->orderBy('platform')->orderBy('package_type') ->forPage($page, $limit)->get(); return [ 'start_date' => $startDate, 'end_date' => $endDate, 'clicks' => ['total' => $clickTotal, 'data' => $clicks], 'opens' => ['total' => $openTotal, 'data' => $opens], ]; }); } public function iosReviews() { return $this->run(function () { $params = request()->validate($this->listRules([ 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'], 'status' => ['nullable', Rule::in([0, 1])], ])); $query = AppIosReviewSetting::query(); if (!empty($params['start_date'])) $query->where('created_at', '>=', $params['start_date'] . ' 00:00:00'); if (!empty($params['end_date'])) $query->where('created_at', '<=', $params['end_date'] . ' 23:59:59'); if (array_key_exists('status', $params) && $params['status'] !== null) $query->where('status', $params['status']); return $this->paginate($query->orderByDesc('id'), $params); }); } public function saveIosReview() { return $this->run(function () { $params = request()->validate([ 'id' => ['nullable', 'integer'], 'store_version' => ['required', 'string', 'max:50'], 'operating_version' => ['required', 'string', 'max:50'], 'review_user_ids' => ['nullable', 'array', 'max:1000'], 'review_user_ids.*' => ['string', 'max:64'], 'status' => ['nullable', Rule::in([0, 1])], ]); $params['platform'] = 'ios'; $params['review_user_ids'] = array_values(array_unique(array_filter($params['review_user_ids'] ?? []))); return $this->saveModel(AppIosReviewSetting::class, $params, 'app_ios_review_setting'); }); } public function iosReviewStatus() { return $this->setStatus(AppIosReviewSetting::class, 'app_ios_review_setting'); } public function deleteIosReview() { return $this->deleteModel(AppIosReviewSetting::class, 'app_ios_review_setting'); } public function logs() { return $this->run(function () { $params = request()->validate($this->listRules([ 'resource_type' => ['required', Rule::in(['app_display_setting', 'app_package', 'app_ios_review_setting'])], 'resource_id' => ['required', 'integer'], 'operator_name' => ['nullable', 'string', 'max:100'], ])); $query = OperationAudit::query()->where('resource_type', $params['resource_type'])->where('resource_id', $params['resource_id']); if (!empty($params['operator_name'])) $query->where('operator_name', 'like', '%' . $params['operator_name'] . '%'); return $this->paginate($query->orderByDesc('id'), $params); }); } private function saveModel(string $modelClass, array $params, string $resourceType): array { return DB::transaction(function () use ($modelClass, $params, $resourceType) { $id = (int)($params['id'] ?? 0); unset($params['id']); $model = $id ? $modelClass::query()->lockForUpdate()->findOrFail($id) : new $modelClass(); $before = $model->exists ? $model->toArray() : []; $model->fill($params + OperationAuditService::actor())->save(); OperationAuditService::record($resourceType, (int)$model->id, $id ? 'update' : 'create', $before, $model); return $model->fresh()->toArray(); }); } private function setStatus(string $modelClass, string $resourceType) { return $this->run(function () use ($modelClass, $resourceType) { $params = request()->validate(['id' => ['required', 'integer'], 'status' => ['required', Rule::in([0, 1])]]); return DB::transaction(function () use ($modelClass, $resourceType, $params) { $model = $modelClass::query()->lockForUpdate()->findOrFail($params['id']); $before = $model->toArray(); $model->fill(['status' => $params['status']] + OperationAuditService::actor())->save(); OperationAuditService::record($resourceType, (int)$model->id, 'status', $before, $model); return $model->fresh()->toArray(); }); }); } private function deleteModel(string $modelClass, string $resourceType) { return $this->run(function () use ($modelClass, $resourceType) { $params = request()->validate(['id' => ['required', 'integer']]); return DB::transaction(function () use ($modelClass, $resourceType, $params) { $model = $modelClass::query()->lockForUpdate()->findOrFail($params['id']); $before = $model->toArray(); $model->delete(); OperationAuditService::record($resourceType, (int)$model->id, 'delete', $before, []); return []; }); }); } private function listRules(array $extra): array { return ['page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1', 'max:200']] + $extra; } private function paginate($query, array $params): array { $page = (int)($params['page'] ?? 1); $limit = (int)($params['limit'] ?? 20); return ['total' => (clone $query)->count(), 'data' => $query->forPage($page, $limit)->get()]; } private function run(callable $callback) { try { return $this->success($callback()); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Throwable $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } } }