feat(admin-frontend): 补齐活跃筛选与支付快照能力

新增用户管理“活跃状态”高级筛选,并在后端支持
activity_status 复合规则,支持按活跃与非活跃筛选用户。

补齐订单支付成功快照落库与后台展示,保存支付渠道、
支付方法、实付金额和支付 IP,并在订单详情中优先展示。

同时增强节点页在线/离线筛选与批量删除、仪表盘快捷入口,
并修复已关闭工单再次回复后自动重开的统一语义。

附带同步测试、迁移、CI 工作流命名及知识库记录
This commit is contained in:
yinjianm
2026-04-25 00:59:08 +08:00
parent 2218457237
commit c64badfc23
55 changed files with 2023 additions and 71 deletions
@@ -0,0 +1,77 @@
<?php
namespace Tests\Unit\Admin;
use App\Http\Controllers\V2\Admin\UserController;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
class UserControllerActivityStatusFilterTest extends TestCase
{
private static ?Capsule $capsule = null;
public function test_resolve_activity_status_value_supports_eq_payloads(): void
{
$controller = new UserController();
$method = new ReflectionMethod(UserController::class, 'resolveActivityStatusValue');
$method->setAccessible(true);
$this->assertTrue($method->invoke($controller, 'eq:1'));
$this->assertFalse($method->invoke($controller, 'eq:0'));
$this->assertNull($method->invoke($controller, 'gte:1'));
}
public function test_active_activity_status_filter_requires_plan_remaining_traffic_and_recent_online(): void
{
$builder = $this->newQueryBuilder();
$this->applyFilter($builder, 'activity_status', 'eq:1');
$sql = $builder->toSql();
$this->assertStringContainsString('"plan_id" is not null', $sql);
$this->assertStringContainsString('COALESCE(transfer_enable, 0) > COALESCE(u, 0) + COALESCE(d, 0)', $sql);
$this->assertStringContainsString('"last_online_at" is not null', $sql);
$this->assertStringContainsString('"last_online_at" >= ?', $sql);
$this->assertCount(1, $builder->getBindings());
}
public function test_inactive_activity_status_filter_uses_reverse_condition_set(): void
{
$builder = $this->newQueryBuilder();
$this->applyFilter($builder, 'activity_status', 'eq:0');
$sql = $builder->toSql();
$this->assertStringContainsString('("plan_id" is null or COALESCE(transfer_enable, 0) <= COALESCE(u, 0) + COALESCE(d, 0) or "last_online_at" is null or "last_online_at" < ?)', $sql);
$this->assertCount(1, $builder->getBindings());
}
private function applyFilter(QueryBuilder $builder, string $field, mixed $value): void
{
$controller = new UserController();
$method = new ReflectionMethod(UserController::class, 'buildFilterQuery');
$method->setAccessible(true);
$method->invoke($controller, $builder, $field, $value);
}
private function newQueryBuilder(): QueryBuilder
{
if (!self::$capsule) {
$capsule = new Capsule();
$capsule->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
self::$capsule = $capsule;
}
return self::$capsule->getConnection()->table('v2_user');
}
}