|
|
@@ -0,0 +1,387 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Controllers\admin;
|
|
|
+
|
|
|
+use App\Constants\HttpStatus;
|
|
|
+use App\Http\Controllers\Controller;
|
|
|
+use App\Models\OperationAudit;
|
|
|
+use App\Models\PaymentCollectionChannel;
|
|
|
+use App\Models\PaymentDirectRecharge;
|
|
|
+use App\Models\PaymentGateway;
|
|
|
+use App\Models\RechargeChannel;
|
|
|
+use App\Models\RechargeChannelGroup;
|
|
|
+use App\Services\OperationAuditService;
|
|
|
+use App\Services\PaymentChannelLinkService;
|
|
|
+use Illuminate\Database\Eloquent\Builder;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Validation\Rule;
|
|
|
+use Illuminate\Validation\ValidationException;
|
|
|
+use Throwable;
|
|
|
+
|
|
|
+class PaymentConfiguration extends Controller
|
|
|
+{
|
|
|
+ private PaymentChannelLinkService $channelLinks;
|
|
|
+
|
|
|
+ public function __construct(PaymentChannelLinkService $channelLinks)
|
|
|
+ {
|
|
|
+ parent::__construct();
|
|
|
+ $this->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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|