| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- <?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 App\Services\Payment\PaymentProviderCatalog;
- 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 () {
- $filters = request()->validate([
- 'kind' => ['nullable', Rule::in(['deposit', 'withdraw', 'collection_scan', 'collection_third_party'])],
- 'payment_company' => ['nullable', Rule::in(PaymentProviderCatalog::codes())],
- 'provider_name' => ['nullable', Rule::in(PaymentProviderCatalog::codes())],
- 'payment_method' => ['nullable', 'string', 'max:64'],
- 'collection_method' => ['nullable', 'string', 'max:64'],
- ]);
- return (new PaymentProviderCatalog())->options(
- RechargeChannel::query()->orderBy('data_type')->orderBy('sort')->orderBy('id')->get()->toArray(),
- RechargeChannelGroup::query()->orderBy('id')->get()->toArray(),
- $filters
- );
- });
- }
- 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', Rule::in(PaymentProviderCatalog::codes())],
- 'merchant_no' => ['required', 'string', 'max:128'],
- 'payment_method' => ['required', 'string', 'max:64'],
- 'channel_code' => ['nullable', 'string', 'max:64'],
- 'channel_name' => ['nullable', 'string', 'max:100'],
- 'withdrawal_secret' => ['nullable', 'string', 'max:10000'],
- 'deposit_secret' => ['nullable', 'string', 'max:10000'],
- 'status' => ['nullable', Rule::in([0, 1])],
- ]);
- $expectedDataType = $params['kind'] === 'withdraw' ? 2 : 1;
- [$channel, $params['recharge_channel_group_ids']] = $this->channelLinks->validate(
- (int)$params['recharge_channel_id'],
- $params['recharge_channel_group_ids'],
- $expectedDataType
- );
- PaymentProviderCatalog::validateSelection($channel->toArray(), $params['payment_company'], $params['payment_method'], $expectedDataType);
- 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', Rule::in(PaymentProviderCatalog::codes())],
- '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])],
- ]);
- [$channel, $params['recharge_channel_group_ids']] = $this->channelLinks->validate(
- (int)$params['recharge_channel_id'],
- $params['recharge_channel_group_ids'],
- 1
- );
- PaymentProviderCatalog::validateSelection($channel->toArray(), $params['provider_name'], $params['collection_method'], 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();
- if ($model instanceof PaymentGateway) $params = $this->gatewayCredentials($model, $params);
- $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 gatewayCredentials(PaymentGateway $model, array $params): array
- {
- if ($model->exists && $model->kind !== $params['kind']) {
- throw ValidationException::withMessages(['kind' => '网关类型创建后不能修改']);
- }
- $secretField = $params['kind'] === 'deposit' ? 'deposit_secret' : 'withdrawal_secret';
- $otherField = $params['kind'] === 'deposit' ? 'withdrawal_secret' : 'deposit_secret';
- $hasSecret = !empty($model->getRawOriginal($secretField));
- $identityChanged = $model->exists && ($model->payment_company !== $params['payment_company']
- || $model->merchant_no !== $params['merchant_no']);
- $newSecret = $params[$secretField] ?? null;
- if ($newSecret === null || $newSecret === '') {
- if (!$hasSecret || $identityChanged) {
- throw ValidationException::withMessages([$secretField => '新增商户或切换公司/商户号时必须填写签名密钥']);
- }
- unset($params[$secretField]);
- }
- $params[$otherField] = null;
- return $params;
- }
- 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());
- }
- }
- }
|