channelLinks = $channelLinks; } public function options() { return $this->run(function () { $groups = RechargeChannelGroup::query()->orderBy('id')->get()->map(fn ($group) => [ 'id' => (int)$group->id, 'name' => (string)$group->name, 'recharge_type' => (array)$group->recharge_type, 'withdraw_type' => (array)$group->withdraw_type, 'activity_type' => (array)$group->activity_type, ]); $channels = RechargeChannel::query()->orderBy('data_type')->orderBy('sort')->orderBy('id') ->get()->map(function ($channel) use ($groups) { $groupField = match ((int)$channel->data_type) { 2 => 'withdraw_type', 3 => 'activity_type', default => 'recharge_type', }; return [ 'id' => (int)$channel->id, 'data_type' => (int)$channel->data_type, 'name' => (string)$channel->name, 'key' => (string)$channel->key, 'type' => (string)$channel->type, 'rate' => number_format((float)$channel->rate, 4, '.', ''), 'fee_rate' => bcmul((string)$channel->rate, '100', 4), 'min_amount' => $channel->min, 'max_amount' => $channel->max, 'fixed_amounts' => $channel->fixed ?: [], 'sort' => (int)$channel->sort, 'status' => (int)$channel->status, 'available_group_ids' => $groups->filter(fn ($group) => in_array( (string)$channel->type, (array)$group[$groupField], true ))->pluck('id')->values(), ]; }); return [ 'recharge_channels' => $channels, 'channel_groups' => $groups, 'levels' => $groups->map(fn ($group) => [ 'value' => $group['id'], 'label' => $group['name'], ])->values(), 'payment_companies' => PaymentGateway::query()->distinct()->orderBy('payment_company')->pluck('payment_company')->filter()->values(), 'payment_methods' => PaymentGateway::query()->distinct()->orderBy('payment_method')->pluck('payment_method')->filter()->values(), 'collection_providers' => PaymentCollectionChannel::query()->distinct()->orderBy('provider_name')->pluck('provider_name')->filter()->values(), 'collection_methods' => PaymentCollectionChannel::query()->distinct()->orderBy('collection_method')->pluck('collection_method')->filter()->values(), ]; }); } public function directList() { return $this->run(function () { $params = request()->validate($this->listRules([ 'name' => ['nullable', 'string', 'max:100'], 'recharge_channel_id' => ['nullable', 'integer'], 'group_id' => ['nullable', 'integer'], 'status' => ['nullable', Rule::in([0, 1])], 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'], ])); $query = PaymentDirectRecharge::query()->with('rechargeChannel'); $this->applyDateAndLike($query, $params, 'name'); $this->applyChannelFilters($query, $params); return $this->paginatePayment($query->orderByDesc('id'), $params); }); } public function directSave() { return $this->run(function () { $params = request()->validate([ 'id' => ['nullable', 'integer'], 'recharge_channel_id' => ['required', 'integer'], 'recharge_channel_group_ids' => ['required', 'array', 'min:1'], 'recharge_channel_group_ids.*' => ['integer'], 'recharge_code' => ['required', 'string', 'max:64'], 'name' => ['required', 'string', 'max:100'], 'amount' => ['required', 'numeric', 'gt:0'], 'status' => ['nullable', Rule::in([0, 1])], ]); [, $params['recharge_channel_group_ids']] = $this->channelLinks->validate( (int)$params['recharge_channel_id'], $params['recharge_channel_group_ids'], 1 ); $id = (int)($params['id'] ?? 0); $duplicate = PaymentDirectRecharge::query()->where('recharge_code', $params['recharge_code']); if ($id) $duplicate->where('id', '<>', $id); if ($duplicate->exists()) throw new \RuntimeException('充值ID已存在'); return $this->saveModel(PaymentDirectRecharge::class, $params, 'payment_direct_recharge'); }); } public function directStatus() { return $this->setStatus(PaymentDirectRecharge::class, 'payment_direct_recharge'); } public function directDelete() { return $this->deleteModel(PaymentDirectRecharge::class, 'payment_direct_recharge'); } public function gatewayList() { return $this->run(function () { $params = request()->validate($this->listRules([ 'kind' => ['required', Rule::in(['deposit', 'withdraw'])], 'recharge_channel_id' => ['nullable', 'integer'], 'group_id' => ['nullable', 'integer'], 'payment_company' => ['nullable', 'string', 'max:100'], 'payment_method' => ['nullable', 'string', 'max:64'], 'status' => ['nullable', Rule::in([0, 1])], ])); $query = PaymentGateway::query()->with('rechargeChannel')->where('kind', $params['kind']); foreach (['payment_company', 'payment_method', 'status'] as $field) { if (array_key_exists($field, $params) && $params[$field] !== null && $params[$field] !== '') { $query->where($field, $params[$field]); } } $this->applyChannelFilters($query, $params); return $this->paginatePayment($query->orderByDesc('id'), $params); }); } public function gatewaySave() { return $this->run(function () { $params = request()->validate([ 'id' => ['nullable', 'integer'], 'recharge_channel_id' => ['required', 'integer'], 'recharge_channel_group_ids' => ['required', 'array', 'min:1'], 'recharge_channel_group_ids.*' => ['integer'], 'kind' => ['required', Rule::in(['deposit', 'withdraw'])], 'currency' => ['nullable', 'string', 'max:16'], 'payment_company' => ['required', 'string', 'max:100'], 'merchant_no' => ['required', 'string', 'max:128'], 'payment_method' => ['nullable', 'string', 'max:64'], 'channel_code' => ['nullable', 'string', 'max:64'], 'channel_name' => ['nullable', 'string', 'max:100'], 'withdrawal_secret' => ['nullable', 'string', 'max:10000'], 'status' => ['nullable', Rule::in([0, 1])], ]); $expectedDataType = $params['kind'] === 'withdraw' ? 2 : 1; [, $params['recharge_channel_group_ids']] = $this->channelLinks->validate( (int)$params['recharge_channel_id'], $params['recharge_channel_group_ids'], $expectedDataType ); $existing = !empty($params['id']) ? PaymentGateway::query()->findOrFail($params['id']) : null; if ($existing && $existing->kind !== $params['kind']) { throw ValidationException::withMessages(['kind' => '网关类型创建后不能修改']); } if ($params['kind'] === 'deposit' && empty($params['payment_method'])) { throw ValidationException::withMessages(['payment_method' => '存款网关必须填写支付方式']); } $hasSavedSecret = $existing ? $existing->has_withdrawal_secret : false; if ($params['kind'] === 'withdraw' && empty($params['withdrawal_secret']) && !$hasSavedSecret) { throw ValidationException::withMessages(['withdrawal_secret' => '出款网关必须填写出款密钥']); } if ($params['kind'] === 'deposit') { $params['withdrawal_secret'] = null; } elseif (empty($params['withdrawal_secret'])) { unset($params['withdrawal_secret']); } return $this->saveModel(PaymentGateway::class, $params, 'payment_gateway'); }); } public function gatewayStatus() { return $this->setStatus(PaymentGateway::class, 'payment_gateway'); } public function gatewayDelete() { return $this->deleteModel(PaymentGateway::class, 'payment_gateway'); } public function collectionList() { return $this->run(function () { $params = request()->validate($this->listRules([ 'kind' => ['required', Rule::in(['collection_scan', 'collection_third_party'])], 'recharge_channel_id' => ['nullable', 'integer'], 'group_id' => ['nullable', 'integer'], 'provider_name' => ['nullable', 'string', 'max:100'], 'collection_method' => ['nullable', 'string', 'max:64'], 'status' => ['nullable', Rule::in([0, 1])], ])); $query = PaymentCollectionChannel::query()->with('rechargeChannel')->where('kind', $params['kind']); foreach (['provider_name', 'collection_method', 'status'] as $field) { if (array_key_exists($field, $params) && $params[$field] !== null && $params[$field] !== '') { $query->where($field, $params[$field]); } } $this->applyChannelFilters($query, $params); return $this->paginatePayment($query->orderByDesc('id'), $params); }); } public function collectionSave() { return $this->run(function () { $params = request()->validate([ 'id' => ['nullable', 'integer'], 'recharge_channel_id' => ['required', 'integer'], 'recharge_channel_group_ids' => ['required', 'array', 'min:1'], 'recharge_channel_group_ids.*' => ['integer'], 'kind' => ['required', Rule::in(['collection_scan', 'collection_third_party'])], 'currency' => ['nullable', 'string', 'max:16'], 'provider_name' => ['required', 'string', 'max:100'], 'group_name' => ['required', 'string', 'max:100'], 'collection_method' => ['required', 'string', 'max:64'], 'channel_code' => ['required', 'string', 'max:64'], 'channel_name' => ['required', 'string', 'max:100'], 'status' => ['nullable', Rule::in([0, 1])], ]); [, $params['recharge_channel_group_ids']] = $this->channelLinks->validate( (int)$params['recharge_channel_id'], $params['recharge_channel_group_ids'], 1 ); return $this->saveModel(PaymentCollectionChannel::class, $params, 'payment_collection_channel'); }); } public function collectionStatus() { return $this->setStatus(PaymentCollectionChannel::class, 'payment_collection_channel'); } public function collectionDelete() { return $this->deleteModel(PaymentCollectionChannel::class, 'payment_collection_channel'); } public function logs() { return $this->run(function () { $params = request()->validate($this->listRules([ 'resource_type' => ['required', Rule::in([ 'payment_direct_recharge', 'payment_gateway', 'payment_collection_channel', ])], 'resource_id' => ['required', 'integer'], 'operator_name' => ['nullable', 'string', 'max:100'], 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'], ])); $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'] . '%'); 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'); 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']); $params += OperationAuditService::actor(); $model = $id ? $modelClass::query()->lockForUpdate()->findOrFail($id) : new $modelClass(); $before = $model->exists ? $model->replicate()->setRawAttributes($model->getRawOriginal()) : []; $model->fill($params); $model->save(); OperationAuditService::record($resourceType, (int)$model->id, $id ? 'update' : 'create', $before, $model); return $this->channelLinks->formatOne($model->fresh()->load('rechargeChannel')); }); } 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 $this->channelLinks->formatOne($model->fresh()->load('rechargeChannel')); }); }); } 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(Builder $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 paginatePayment(Builder $query, array $params): array { $page = (int)($params['page'] ?? 1); $limit = (int)($params['limit'] ?? 20); $total = (clone $query)->count(); $models = $query->forPage($page, $limit)->get(); return ['total' => $total, 'data' => $this->channelLinks->formatMany($models)]; } private function applyChannelFilters(Builder $query, array $params): void { if (!empty($params['recharge_channel_id'])) { $query->where('recharge_channel_id', (int)$params['recharge_channel_id']); } if (!empty($params['group_id'])) { $query->whereJsonContains('recharge_channel_group_ids', (int)$params['group_id']); } if (array_key_exists('status', $params) && $params['status'] !== null && $params['status'] !== '') { $query->where('status', (int)$params['status']); } } private function applyDateAndLike(Builder $query, array $params, string $likeField): void { if (!empty($params[$likeField])) $query->where($likeField, 'like', '%' . $params[$likeField] . '%'); 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'); } 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()); } } }