# Kylin Activity

Kylin 活动日志包 — Fork 自 spatie/laravel-activitylog，为企业级 Laravel 应用提供合规的审计日志能力。

## 功能特性

- **自动记录**：模型事件（created/updated/deleted/restored）自动记录活动日志
- **手动记录**：通过 `activity()` 辅助函数手动记录自定义活动
- **属性追踪**：记录模型属性变更前后对比（old → new）
- **敏感信息防护**：自动脱敏密码、身份证号、银行卡号等敏感字段（依据《个保法》28条）
- **日志防篡改**：审计日志不可更新/删除，仅允许通过命令批量清理（依据等保 2.0）
- **多租户隔离**：支持按租户 ID 隔离日志数据
- **批次管理**：同一请求/操作的多条日志归入同一 UUID 批次
- **日志清理**：Artisan 命令清理过期日志，强制最小保留 180 天（依据《网安法》21条）
- **全局开关**：支持运行时启用/禁用日志记录
- **变更管道**：支持自定义 Pipeline 处理变更数据

## 安装

```bash
composer require kylin/activity
```

### 发布配置

```bash
php artisan vendor:publish --tag=kylin-activity-config
```

### 运行迁移

```bash
php artisan migrate
```

迁移文件位于 `database/migrations/`，包含：
- `create_activity_log_table.php.stub` — 创建活动日志表
- `add_event_column_to_activity_log_table.php.stub` — 添加事件列
- `add_batch_uuid_column_to_activity_log_table.php.stub` — 添加批次 UUID 列

## 配置

配置文件：`config/kylin-activity.php`

| 配置项 | 类型 | 默认值 | 说明 |
|------|------|------|------|
| `enabled` | bool | `true` | 全局启用/禁用日志记录 |
| `delete_records_older_than_days` | int | `365` | 日志保留天数（>= 180 天） |
| `default_log_name` | string | `'default'` | 默认日志名称 |
| `default_auth_driver` | string\|null | `null` | 认证驱动（null 使用默认） |
| `subject_returns_soft_deleted_models` | bool | `false` | 是否返回软删除的主体 |
| `activity_model` | string | `\Kylin\Activity\Models\Activity::class` | 活动日志模型 |
| `table_name` | string | `'activity_log'` | 日志表名 |
| `database_connection` | string\|null | `null` | 数据库连接（null 使用默认） |
| `sensitive_fields` | array | 见配置文件 | 敏感字段列表（用于脱敏） |
| `multi_tenant` | array | 见配置文件 | 多租户配置 |
| `immutable_logs` | bool | `true` | 日志防篡改开关 |

### 敏感字段配置

```php
'sensitive_fields' => [
    'password', 'password_confirmation', 'old_password', 'new_password',
    'id_card', 'id_card_number', 'id_number',
    'bank_card', 'bank_account', 'bank_card_number', 'credit_card',
    'cvv', 'cvc', 'security_code', 'verification_code',
    'token', 'api_key', 'api_secret', 'secret_key', 'private_key',
    'access_token', 'refresh_token', 'session_id',
    'biometric', 'fingerprint', 'face_data',
    'health_info', 'medical_record', 'genetic',
],
```

### 多租户配置

```php
'multi_tenant' => [
    'enabled' => env('ACTIVITY_LOGGER_MULTI_TENANT', false),
    'tenant_field' => 'tenant_id',
    'tenant_resolver' => null, // 自定义租户解析回调
],
```

## 使用示例

### 模型自动日志

```php
use Kylin\Activity\Traits\LogsActivity;
use Kylin\Activity\LogOptions;

class Article extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['title', 'content', 'status'])  // 仅记录指定字段
            ->logOnlyDirty()                            // 仅记录变更字段
            ->useLogName('article');                    // 日志分类名称
    }
}
```

### 手动记录

```php
// 基础用法
activity()->log('系统启动');

// 关联操作主体
activity()
    ->performedOn($article)
    ->log('文章已发布');

// 关联操作者
activity()
    ->performedOn($order)
    ->causedBy(auth()->user())
    ->withProperties(['amount' => 199.00, 'status' => 'paid'])
    ->log('订单已支付');

// 指定日志名称
activity('payment')
    ->causedBy($user)
    ->withProperty('transaction_id', 'TXN123456')
    ->log('支付成功');

// 匿名操作者
activity()
    ->performedOn($model)
    ->causedByAnonymous()
    ->log('系统自动操作');
```

### 日志选项（LogOptions）

```php
LogOptions::defaults()
    ->logAll()                                    // 记录所有属性
    ->logOnly(['title', 'content'])              // 仅记录指定属性
    ->logExcept(['password', 'token'])           // 排除指定属性
    ->logOnlyDirty()                              // 仅记录变更属性
    ->logFillable()                               // 记录所有 $fillable 属性
    ->logUnguarded()                              // 记录所有非 $guarded 属性
    ->dontSubmitEmptyLogs()                       // 忽略无变更日志
    ->dontLogIfAttributesChangedOnly(['updated_at']) // 忽略仅指定属性变更
    ->useLogName('custom')                        // 自定义日志名称
    ->setDescriptionForEvent(function (string $eventName) {
        return match ($eventName) {
            'created' => '文章已创建',
            'updated' => '文章已更新',
            'deleted' => '文章已删除',
            default => $eventName,
        };
    })
    ->useAttributeRawValues(['json_field']);      // 指定属性不转换类型
```

### 查询日志

```php
use Kylin\Activity\Models\Activity;

// 按模型查询
$activities = Activity::forSubject($article)->get();

// 按操作者查询
$activities = Activity::causedBy($user)->get();

// 按日志名称查询
$activities = Activity::inLog('article', 'payment')->get();

// 按事件类型查询
$activities = Activity::forEvent('created')->get();

// 按批次查询
$activities = Activity::forBatch($batchUuid)->get();

// 多租户隔离查询
$activities = Activity::forTenant($tenantId)->get();

// 获取属性变更
$activity = Activity::first();
$changes = $activity->changes();  // ['attributes' => [...], 'old' => [...]]
$extra = $activity->getExtraProperty('amount', 0);
```

### 批次管理

```php
use Kylin\Activity\Facades\LogBatch;

// 闭包内批量操作归入同一批次
$batchUuid = LogBatch::withinBatch(function ($uuid) {
    activity()->log('步骤一');
    activity()->log('步骤二');
    activity()->log('步骤三');
    return $uuid;
});

// 手动设置批次
LogBatch::setBatch('custom-uuid');
activity()->log('操作一');
activity()->log('操作二');
LogBatch::endBatch();
```

### 全局禁用/启用

```php
// 临时禁用日志
activity()->withoutLogs(function () {
    // 此闭包内的操作不会记录日志
    User::create(['name' => 'Test']);
});

// 手动切换
activity()->disableLogging();
// ... 不记录日志的操作
activity()->enableLogging();
```

### 自定义变更管道

```php
use Kylin\Activity\Contracts\LoggablePipe;
use Kylin\Activity\EventLogBag;

class HashPasswordPipe implements LoggablePipe
{
    public function handle(EventLogBag $event, Closure $next): EventLogBag
    {
        // 从变更数据中移除密码字段
        unset($event->changes['attributes']['password']);

        return $next($event);
    }
}

// 在模型中注册
Article::addLogChange(new HashPasswordPipe());
```

### 清理过期日志

```bash
# 清理所有超过 365 天的日志
php artisan activitylog:clean

# 清理指定日志名称的过期记录
php artisan activitylog:clean payment

# 指定保留天数（最小 180 天）
php artisan activitylog:clean --days=180

# 生产环境强制执行
php artisan activitylog:clean --force
```

### 审计脱敏质量

```bash
# 审计最近 30 天的日志脱敏质量
php artisan activitylog:audit-redaction

# 指定扫描天数
php artisan activitylog:audit-redaction --days=90

# 自动修复检测到的问题
php artisan activitylog:audit-redaction --fix
```

### 日志查看审计中间件

```php
// 在路由中使用中间件自动记录日志查看行为
Route::get('/admin/activity-logs', [ActivityLogController::class, 'index'])
    ->middleware('activity-log-viewer');

// 或指定日志分类名称
Route::get('/admin/activity-logs', [ActivityLogController::class, 'index'])
    ->middleware('activity-log-viewer:security-audit');
```

## 模型关联

在业务模型中添加 `activities()` 关联：

```php
class Article extends Model
{
    use LogsActivity;

    public function activities(): MorphMany
    {
        return $this->morphMany(
            \Kylin\Activity\Models\Activity::class,
            'subject'
        );
    }
}

// 使用
$article->activities()->latest()->get();
```

## 数据库表结构

| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| log_name | varchar(255) | 日志分类名称 |
| description | text | 日志描述 |
| subject_type | varchar(255) | 操作主体多态类型 |
| subject_id | bigint | 操作主体 ID |
| causer_type | varchar(255) | 操作者多态类型 |
| causer_id | bigint | 操作者 ID |
| event | varchar(255) | 事件类型 |
| batch_uuid | char(36) | 批次 UUID |
| properties | json | 属性快照 |
| tenant_id | bigint | 租户 ID（多租户场景） |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |

## 架构设计

```
┌─────────────────────────────────────────────────────┐
│                    业务模型 (Model)                    │
│  use LogsActivity;                                   │
│  getActivitylogOptions(): LogOptions                  │
└──────────────┬──────────────────────────────────────┘
               │ 事件触发
               ▼
┌─────────────────────────────────────────────────────┐
│                 LogsActivity Trait                    │
│  bootLogsActivity() → 监听 created/updated/deleted   │
│  属性变更对比 → 构建 EventLogBag                      │
│  变更管道处理 → 调用 ActivityLogger                   │
└──────────────┬──────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────┐
│                 ActivityLogger                        │
│  performedOn() / causedBy() / withProperties()       │
│  redactSensitiveData() ← 敏感信息脱敏                 │
│  log() → 保存到数据库                                 │
└──────────────┬──────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────┐
│              Activity Model (Eloquent)                │
│  booted() → 防篡改 (禁止 update/delete)              │
│  scopeForTenant() → 多租户隔离                       │
│  scopeInLog() / causedBy() / forSubject() / forEvent()│
└─────────────────────────────────────────────────────┘
```

## 合规要点

| 法规 | 要求 | 实现方式 |
|------|------|----------|
| 《网安法》21条 | 日志留存 >= 180 天 | `delete_records_older_than_days = 365`，清理命令强制最小 180 天 |
| 《个保法》28条 | 禁止记录敏感个人信息 | `sensitive_fields` 配置 + `redactSensitiveData()` 自动脱敏 |
| 等保 2.0 | 审计记录不可删除/修改 | `immutable_logs` + `booted()` 防篡改 |
| 《数据安全法》21条 | 数据分类分级 | 支持独立数据库连接和表名配置 |

## 依赖

- PHP ^8.5
- Laravel ^13.0
- kylin/core

## 许可证

私有软件，未经授权禁止使用。