1, 'withdraw_type' => 2, 'activity_type' => 3] as $field => $direction) { if (!array_key_exists($field, $params)) continue; $params[$field] = $this->normalizeTypes((array)($params[$field] ?? [])); // Match deletion's lock order: channel rows, then the group being edited. $validTypes[$field] = $params[$field] === [] ? [] : RechargeChannel::query() ->where('data_type', $direction)->whereIn('type', $params[$field]) ->orderBy('id')->lockForUpdate()->get(['type'])->pluck('type')->map(fn ($type) => (string)$type)->all(); } $group = $id ? RechargeChannelGroup::query()->lockForUpdate()->findOrFail($id) : new RechargeChannelGroup(); $before = $group->exists ? $group->toArray() : []; $removed = []; foreach ($validTypes as $field => $valid) { $invalid = array_values(array_diff($params[$field], $valid)); $previous = $group->exists ? $this->normalizeTypes((array)$group->{$field}) : []; $newInvalid = array_values(array_diff($invalid, $previous)); if ($newInvalid !== []) { throw new RuntimeException($field . ' 包含不存在或已删除的通道类型:' . implode(',', $newInvalid) . ';请提交选项的 type,不是通道 ID 或公司代码'); } if ($invalid !== []) $removed[$field] = $invalid; $params[$field] = array_values(array_intersect($params[$field], $valid)); } $group->fill($params); $group->save(); if ($removed !== []) { OperationAuditService::record('recharge_channel_group', (int)$group->id, 'update', $before, ['group' => $group->toArray(), 'removed_types' => $removed]); } return $removed; }, 3); } public function deleteGroup(int $id): void { if ($id === 1) throw new RuntimeException('默认通道组合不能删除'); DB::transaction(function () use ($id) { $group = RechargeChannelGroup::withTrashed()->lockForUpdate()->findOrFail($id); if ($group->trashed()) return; if (Schema::hasTable('users') && Schema::hasColumn('users', 'recharge_channel_group_id') && DB::table('users')->where('recharge_channel_group_id', $id)->lockForUpdate()->first(['id'])) { throw new RuntimeException('该层级仍有会员使用,请先将会员调整到其他层级'); } foreach (['payment_gateways', 'payment_collection_channels', 'payment_direct_recharges'] as $table) { if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'recharge_channel_group_ids')) continue; DB::table($table)->whereNull('deleted_at')->whereNotNull('recharge_channel_group_ids') ->select(['id', 'recharge_channel_group_ids']) ->orderBy('id')->lockForUpdate()->chunkById(200, function ($rows) use ($id) { foreach ($rows as $row) { // Match runtime's integer normalization, including legacy string IDs such as "02". $ids = array_map('intval', (array)json_decode($row->recharge_channel_group_ids, true)); if (in_array($id, $ids, true)) throw new RuntimeException('该层级仍被支付配置引用,请先解除关联'); } }, 'id', 'id'); } $before = $group->toArray(); $group->delete(); OperationAuditService::record('recharge_channel_group', $id, 'delete', $before, $group); }, 3); } private function normalizeTypes(array $values): array { $types = []; foreach ($values as $value) { if ($value !== null && !is_string($value) && !is_int($value)) { throw new RuntimeException('通道类型应提交 type 字符串数组,不能提交选项对象'); } $type = trim((string)$value); if ($type !== '') $types[] = $type; } return array_values(array_unique($types)); } public function options(int $dataType, ?int $id = null): array { $options = PaymentProviderCatalog::channelFormOptions($dataType, PaymentProviderService::statuses()); $reason = ''; if ($id) { $channel = RechargeChannel::query()->findOrFail($id); if ((int)$channel->data_type !== $dataType) throw new RuntimeException('通道方向与编辑记录不一致'); $reason = $this->identityLockReason($channel); } return $options + ['identity_editable' => $reason === '', 'identity_edit_reason' => $reason]; } public function save(array $params): RechargeChannel { return DB::transaction(function () use ($params) { $id = (int)($params['id'] ?? 0); unset($params['id']); $channel = $id ? RechargeChannel::query()->lockForUpdate()->findOrFail($id) : new RechargeChannel(); if ($channel->exists && (int)$channel->data_type !== (int)$params['data_type']) { throw new RuntimeException('不能修改通道所属的充值、提现或活动方向'); } $params = PaymentProviderCatalog::normalizeChannelSelection($params); $identityChanged = !$channel->exists || (int)$channel->from !== $params['from'] || (string)$channel->type !== (string)$params['type']; $identity = PaymentProviderCatalog::channelIdentity($params); if ($identityChanged && $identity) PaymentProviderService::assertEnabled($identity['provider_code']); if ($channel->exists && ((int)$channel->from !== $params['from'] || (string)$channel->type !== (string)$params['type'])) { $reason = $this->identityLockReason($channel, true); if ($reason !== '') throw new RuntimeException($reason); } foreach (['name', 'key'] as $field) { if (array_key_exists($field, $params)) $params[$field] = trim((string)$params[$field]); if ((!$channel->exists || array_key_exists($field, $params)) && ($params[$field] ?? '') === '') { throw new RuntimeException($field === 'name' ? '请填写通道名称' : '请填写通道标识Key'); } } foreach (['status', 'sort'] as $field) { if (array_key_exists($field, $params) && $params[$field] === null) unset($params[$field]); } if (!$channel->exists) $params += ['status' => 1, 'sort' => 0]; $channel->fill($params); $channel->save(); return $channel; }, 3); } public function delete(int $id): void { DB::transaction(function () use ($id) { $channel = RechargeChannel::withTrashed()->lockForUpdate()->findOrFail($id); if (!in_array((int)$channel->data_type, [1, 2], true)) throw new RuntimeException('只支持删除充值或提现通道'); if ($channel->trashed()) return; if ($this->identityLockReason($channel, true) !== '') { throw new RuntimeException('该通道已被支付配置引用,请先解除关联再删除'); } $before = $channel->toArray(); $channel->status = 0; $channel->save(); $channel->delete(); // Groups grant access by type, so retain the grant while another row of that type remains. $remaining = RechargeChannel::query()->where('data_type', $channel->data_type) ->where('type', $channel->type)->lockForUpdate()->first(['id']); $groupChanges = []; if (!$remaining) { $field = (int)$channel->data_type === 1 ? 'recharge_type' : 'withdraw_type'; foreach (RechargeChannelGroup::query()->orderBy('id')->lockForUpdate()->get() as $group) { $types = (array)$group->{$field}; $updated = array_values(array_filter($types, fn ($type) => trim((string)$type) !== (string)$channel->type)); if ($types === $updated) continue; $groupChanges[] = ['id' => (int)$group->id, 'field' => $field, 'before' => $types, 'after' => $updated]; $group->{$field} = $updated; $group->save(); } } OperationAuditService::record('recharge_channel', $id, 'delete', $before, ['channel' => $channel->toArray(), 'group_changes' => $groupChanges]); }, 3); } private function identityLockReason(RechargeChannel $channel, bool $lock = false): string { foreach (['payment_gateways', 'payment_collection_channels', 'payment_direct_recharges'] as $table) { if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'recharge_channel_id')) continue; $query = DB::table($table)->where('recharge_channel_id', $channel->id)->whereNull('deleted_at'); if ($lock) $query->lockForUpdate(); if ($query->first(['id'])) return '该通道已被支付配置引用,请先解除关联或新增通道,再更换公司或类型'; } return ''; } }