|
|
@@ -3,6 +3,7 @@
|
|
|
namespace App\Services\Payment;
|
|
|
|
|
|
use App\Models\RechargeChannel;
|
|
|
+use App\Models\OperationAudit;
|
|
|
use App\Models\RechargeChannelGroup;
|
|
|
use App\Services\OperationAuditService;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
@@ -17,7 +18,7 @@ class RechargeChannelConfigurationService
|
|
|
$id = (int)($params['id'] ?? 0);
|
|
|
unset($params['id']);
|
|
|
$validTypes = [];
|
|
|
- foreach (['recharge_type' => 1, 'withdraw_type' => 2, 'activity_type' => 3] as $field => $direction) {
|
|
|
+ foreach (['recharge_type' => 1, 'withdraw_type' => 2] 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.
|
|
|
@@ -110,8 +111,10 @@ class RechargeChannelConfigurationService
|
|
|
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('不能修改通道所属的充值、提现或活动方向');
|
|
|
+ throw new RuntimeException('不能修改通道所属的充值或提现方向');
|
|
|
}
|
|
|
+ $isNew = !$channel->exists;
|
|
|
+ $before = $isNew ? [] : $channel->toArray();
|
|
|
$params = PaymentProviderCatalog::normalizeChannelSelection($params);
|
|
|
$params = $this->amountConfiguration($params, $channel);
|
|
|
$identityChanged = !$channel->exists || (int)$channel->from !== $params['from'] || (string)$channel->type !== (string)$params['type'];
|
|
|
@@ -143,6 +146,10 @@ class RechargeChannelConfigurationService
|
|
|
if (RechargeChannelLogoService::supportsDirection((int)$channel->data_type) && array_key_exists('logo', $params)) {
|
|
|
RechargeChannelLogoService::store((string)$channel->type, $params['logo'], (int)$channel->data_type);
|
|
|
}
|
|
|
+ $after = $channel->fresh()->toArray();
|
|
|
+ if ($isNew || self::diff($before, $after) !== []) {
|
|
|
+ OperationAuditService::record('recharge_channel', (int)$channel->id, $isNew ? 'create' : 'update', $before, $after);
|
|
|
+ }
|
|
|
return $channel;
|
|
|
}, 3);
|
|
|
}
|
|
|
@@ -218,6 +225,96 @@ class RechargeChannelConfigurationService
|
|
|
}, 3);
|
|
|
}
|
|
|
|
|
|
+ /** 通道操作日志中展示的字段(顺序即展示顺序) */
|
|
|
+ private const AUDIT_FIELDS = [
|
|
|
+ 'name' => '通道名称', 'key' => '通道标识Key', 'from' => '来源', 'type' => '通道类型', 'rate' => '费率',
|
|
|
+ 'min' => '最低金额', 'max' => '最高金额', 'fixed' => '固定金额', 'status' => '状态', 'sort' => '排序', 'logo' => 'Logo',
|
|
|
+ ];
|
|
|
+ private const AUDIT_ACTIONS = ['create' => '新增', 'update' => '修改', 'delete' => '删除', 'save' => '修改Logo'];
|
|
|
+
|
|
|
+ /** 充值(1)/提现(2)通道操作日志:谁、什么时间、改了什么 */
|
|
|
+ public function logs(array $params): array
|
|
|
+ {
|
|
|
+ $dataType = (int)$params['data_type'];
|
|
|
+ $channels = RechargeChannel::withTrashed()->where('data_type', $dataType)->get(['id', 'name', 'type'])->keyBy('id');
|
|
|
+ $query = OperationAudit::query()->where(function ($q) use ($channels, $dataType) {
|
|
|
+ $q->where('resource_type', 'recharge_channel')->whereIn('resource_id', $channels->keys()->all() ?: [0]);
|
|
|
+ // 充值方式Logo按充值类型统一配置,归入充值通道日志
|
|
|
+ if ($dataType === 1) $q->orWhere('resource_type', 'recharge_channel_logo');
|
|
|
+ });
|
|
|
+ if (!empty($params['channel_id'])) {
|
|
|
+ $query->where('resource_type', 'recharge_channel')->where('resource_id', (int)$params['channel_id']);
|
|
|
+ }
|
|
|
+ if (!empty($params['operator_name'])) $query->where('operator_name', 'like', '%' . trim($params['operator_name']) . '%');
|
|
|
+ if (!empty($params['action'])) $query->where('action', $params['action']);
|
|
|
+ if (!empty($params['start_time'])) $query->where('created_at', '>=', $params['start_time']);
|
|
|
+ if (!empty($params['end_time'])) $query->where('created_at', '<=', $params['end_time']);
|
|
|
+ $total = (clone $query)->count();
|
|
|
+ $rows = $query->orderByDesc('id')->forPage((int)($params['page'] ?? 1), (int)($params['limit'] ?? 20))->get();
|
|
|
+ $data = $rows->map(function (OperationAudit $log) use ($channels) {
|
|
|
+ $changes = (array)$log->changes;
|
|
|
+ $isLogo = $log->resource_type === 'recharge_channel_logo';
|
|
|
+ if ($isLogo) {
|
|
|
+ $before = ['logo' => $changes['before']['logo'] ?? ''];
|
|
|
+ $after = ['logo' => $changes['after']['logo'] ?? ''];
|
|
|
+ $type = (string)($changes['after']['channel_type'] ?? $changes['before']['channel_type'] ?? '');
|
|
|
+ } else {
|
|
|
+ $before = (array)($changes['before'] ?? []);
|
|
|
+ // 删除日志的 after 为 {channel, group_changes}
|
|
|
+ $after = $log->action === 'delete' ? [] : (array)($changes['after'] ?? []);
|
|
|
+ $type = (string)($after['type'] ?? $before['type'] ?? '');
|
|
|
+ }
|
|
|
+ $channel = $isLogo ? null : $channels->get($log->resource_id);
|
|
|
+ $diff = self::diff($before, $after);
|
|
|
+ $created = $log->getRawOriginal('created_at');
|
|
|
+ return [
|
|
|
+ 'id' => (int)$log->id,
|
|
|
+ 'channel_id' => $isLogo ? null : (int)$log->resource_id,
|
|
|
+ 'channel_name' => (string)($after['name'] ?? $before['name'] ?? ($channel->name ?? '')),
|
|
|
+ 'channel_type' => $type,
|
|
|
+ 'action' => $isLogo ? 'logo' : (string)$log->action,
|
|
|
+ 'action_label' => self::AUDIT_ACTIONS[$log->action] ?? (string)$log->action,
|
|
|
+ 'operator_id' => $log->operator_id === null ? null : (int)$log->operator_id,
|
|
|
+ 'operator_name' => (string)$log->operator_name,
|
|
|
+ 'created_at' => $created ? date('Y-m-d H:i:s', strtotime((string)$created)) : null,
|
|
|
+ 'changes' => $diff,
|
|
|
+ 'summary' => implode(';', array_map(fn ($c) => "{$c['label']}:{$c['before_text']} → {$c['after_text']}", $diff)),
|
|
|
+ ];
|
|
|
+ })->all();
|
|
|
+ return ['total' => $total, 'data' => $data];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function diff(array $before, array $after): array
|
|
|
+ {
|
|
|
+ $result = [];
|
|
|
+ foreach (self::AUDIT_FIELDS as $field => $label) {
|
|
|
+ if (!array_key_exists($field, $before) && !array_key_exists($field, $after)) continue;
|
|
|
+ $old = self::auditValue($field, $before[$field] ?? null);
|
|
|
+ $new = self::auditValue($field, $after[$field] ?? null);
|
|
|
+ if ($old === $new) continue;
|
|
|
+ $result[] = ['field' => $field, 'label' => $label, 'before' => $old, 'after' => $new,
|
|
|
+ 'before_text' => self::auditText($field, $old), 'after_text' => self::auditText($field, $new)];
|
|
|
+ }
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function auditValue(string $field, $value)
|
|
|
+ {
|
|
|
+ if ($value === null || $value === '' || $value === []) return null;
|
|
|
+ if ($field === 'fixed') return implode(',', is_array($value) ? $value : explode(',', (string)$value));
|
|
|
+ if (in_array($field, ['from', 'status', 'sort'], true)) return (int)$value;
|
|
|
+ if (in_array($field, ['rate', 'min', 'max'], true)) return rtrim(rtrim(bcadd((string)$value, '0', 4), '0'), '.');
|
|
|
+ return (string)$value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function auditText(string $field, $value): string
|
|
|
+ {
|
|
|
+ if ($value === null) return '空';
|
|
|
+ if ($field === 'status') return $value === 1 ? '启用' : '停用';
|
|
|
+ if ($field === 'from') return [1 => '第三方支付', 2 => 'USDT', 3 => '人工'][$value] ?? (string)$value;
|
|
|
+ return (string)$value;
|
|
|
+ }
|
|
|
+
|
|
|
private function identityLockReason(RechargeChannel $channel, bool $lock = false): string
|
|
|
{
|
|
|
foreach (['payment_gateways', 'payment_collection_channels', 'payment_direct_recharges'] as $table) {
|