update user list
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Model\User;
|
||||||
|
use Hyperf\Command\Annotation\Command;
|
||||||
|
use Hyperf\Command\Command as HyperfCommand;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
|
#[Command]
|
||||||
|
class UserListCommand extends HyperfCommand
|
||||||
|
{
|
||||||
|
public function __construct(protected ContainerInterface $container)
|
||||||
|
{
|
||||||
|
parent::__construct('user:list');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure(): void
|
||||||
|
{
|
||||||
|
parent::configure();
|
||||||
|
$this->setDescription('List all system users');
|
||||||
|
$this->addOption('role', 'r', InputOption::VALUE_OPTIONAL, 'Filter by role name (e.g. administrator, developer, accessor)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$query = User::query()->with('role');
|
||||||
|
|
||||||
|
$role_name = $this->input->getOption('role');
|
||||||
|
if ($role_name) {
|
||||||
|
$query->whereHas('role', fn ($q) => $q->where('name', $role_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $query->orderBy('id')->get();
|
||||||
|
|
||||||
|
if ($users->isEmpty()) {
|
||||||
|
$this->info('No users found.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $users->map(fn (User $u) => [
|
||||||
|
$u->id,
|
||||||
|
$u->username,
|
||||||
|
$u->email,
|
||||||
|
$u->role?->name ?? '-',
|
||||||
|
$this->formatStatus($u->status),
|
||||||
|
$u->created_at?->toDateTimeString(),
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
|
$this->table(
|
||||||
|
['ID', 'Username', 'Email', 'Role', 'Status', 'Created At'],
|
||||||
|
$rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->info(sprintf('Total: %d user(s)', count($rows)));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatStatus(int $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
1 => '<fg=green>enabled</>',
|
||||||
|
0 => '<fg=red>disabled</>',
|
||||||
|
default => '<fg=yellow>unknown</>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Model\User;
|
||||||
|
use Hyperf\Command\Annotation\Command;
|
||||||
|
use Hyperf\Command\Command as HyperfCommand;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
|
#[Command]
|
||||||
|
class UserResetPasswordCommand extends HyperfCommand
|
||||||
|
{
|
||||||
|
public function __construct(protected ContainerInterface $container)
|
||||||
|
{
|
||||||
|
parent::__construct('user:reset-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure(): void
|
||||||
|
{
|
||||||
|
parent::configure();
|
||||||
|
$this->setDescription('Reset password for a specified user');
|
||||||
|
$this->addArgument('identifier', InputArgument::REQUIRED, 'Username or user ID');
|
||||||
|
$this->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'New password (min 6 characters). If omitted, will prompt interactively');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$identifier = $this->input->getArgument('identifier');
|
||||||
|
|
||||||
|
// 按 ID(纯数字)或 username 查找用户
|
||||||
|
$user = is_numeric($identifier)
|
||||||
|
? User::find((int) $identifier)
|
||||||
|
: User::query()->where('username', $identifier)->first();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
$this->error(sprintf('User "%s" not found.', $identifier));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取密码:优先 --password 选项,否则交互式隐藏输入
|
||||||
|
$new_password = $this->input->getOption('password');
|
||||||
|
if ($new_password === null) {
|
||||||
|
$new_password = $this->secret('Enter new password');
|
||||||
|
if ($new_password === null) {
|
||||||
|
$this->error('Password cannot be empty.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码长度校验(bcrypt 截断超过 72 字节的输入)
|
||||||
|
if (strlen($new_password) < 6) {
|
||||||
|
$this->error('Password must be at least 6 characters.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strlen($new_password) > 72) {
|
||||||
|
$this->error('Password must not exceed 72 characters (bcrypt limit).');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// password mutator 自动调用 password_hash()
|
||||||
|
$user->password = $new_password;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->info(sprintf("Password for user '%s' has been reset.", $user->username));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace HyperfTest\Cases\Unit\Command;
|
||||||
|
|
||||||
|
use App\Command\UserListCommand;
|
||||||
|
use Hyperf\Context\ApplicationContext;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserListCommand 单元测试
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class UserListCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
private function runInCoroutine(callable $callback): void
|
||||||
|
{
|
||||||
|
if (\Swoole\Coroutine::getCid() > 0) {
|
||||||
|
$callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exception = null;
|
||||||
|
\Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
|
||||||
|
try {
|
||||||
|
$callback();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$exception = $e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ($exception !== null) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_all_users_displays_table(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserListCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute([]);
|
||||||
|
|
||||||
|
$output = $tester->getDisplay();
|
||||||
|
$this->assertStringContainsString('ID', $output);
|
||||||
|
$this->assertStringContainsString('Username', $output);
|
||||||
|
$this->assertStringContainsString('Email', $output);
|
||||||
|
$this->assertStringContainsString('Role', $output);
|
||||||
|
$this->assertStringContainsString('Status', $output);
|
||||||
|
$this->assertStringContainsString('Total:', $output);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_users_filtered_by_role(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserListCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute(['--role' => 'administrator']);
|
||||||
|
|
||||||
|
$output = $tester->getDisplay();
|
||||||
|
$this->assertTrue(
|
||||||
|
str_contains($output, 'administrator') || str_contains($output, 'No users found'),
|
||||||
|
'Should display administrator users or empty message'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_users_with_nonexistent_role_shows_empty(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserListCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute(['--role' => 'nonexistent_role_xyz_123']);
|
||||||
|
|
||||||
|
$output = $tester->getDisplay();
|
||||||
|
$this->assertStringContainsString('No users found', $output);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace HyperfTest\Cases\Unit\Command;
|
||||||
|
|
||||||
|
use App\Command\UserResetPasswordCommand;
|
||||||
|
use App\Model\Role;
|
||||||
|
use App\Model\User;
|
||||||
|
use Hyperf\Context\ApplicationContext;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserResetPasswordCommand 单元测试
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class UserResetPasswordCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
private ?int $testUserId = null;
|
||||||
|
|
||||||
|
private function runInCoroutine(callable $callback): void
|
||||||
|
{
|
||||||
|
if (\Swoole\Coroutine::getCid() > 0) {
|
||||||
|
$callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exception = null;
|
||||||
|
\Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
|
||||||
|
try {
|
||||||
|
$callback();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$exception = $e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ($exception !== null) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if ($this->testUserId !== null) {
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
User::query()->where('id', $this->testUserId)->delete();
|
||||||
|
});
|
||||||
|
$this->testUserId = null;
|
||||||
|
}
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTestUser(): User
|
||||||
|
{
|
||||||
|
$role = Role::query()->first();
|
||||||
|
$unique = 'test_reset_' . uniqid();
|
||||||
|
$user = User::create([
|
||||||
|
'username' => $unique,
|
||||||
|
'email' => $unique . '@test.com',
|
||||||
|
'password' => 'old_password_123',
|
||||||
|
'status' => 1,
|
||||||
|
'role_id' => $role?->id,
|
||||||
|
'ext' => [],
|
||||||
|
]);
|
||||||
|
$this->testUserId = $user->id;
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reset_password_by_username(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$test_user = $this->createTestUser();
|
||||||
|
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserResetPasswordCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute([
|
||||||
|
'identifier' => $test_user->username,
|
||||||
|
'--password' => 'new_password_123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $tester->getStatusCode());
|
||||||
|
$this->assertStringContainsString('has been reset', $tester->getDisplay());
|
||||||
|
|
||||||
|
// 验证密码确实已更新
|
||||||
|
$updated_user = User::find($test_user->id);
|
||||||
|
$this->assertTrue($updated_user->verifyPassword('new_password_123'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reset_password_by_id(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$test_user = $this->createTestUser();
|
||||||
|
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserResetPasswordCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute([
|
||||||
|
'identifier' => (string) $test_user->id,
|
||||||
|
'--password' => 'another_pwd_456',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $tester->getStatusCode());
|
||||||
|
$this->assertStringContainsString('has been reset', $tester->getDisplay());
|
||||||
|
|
||||||
|
$updated_user = User::find($test_user->id);
|
||||||
|
$this->assertTrue($updated_user->verifyPassword('another_pwd_456'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_user_not_found_shows_error(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserResetPasswordCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute([
|
||||||
|
'identifier' => 'nonexistent_user_xyz_999',
|
||||||
|
'--password' => 'some_password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(1, $tester->getStatusCode());
|
||||||
|
$this->assertStringContainsString('not found', $tester->getDisplay());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_password_too_short_shows_error(): void
|
||||||
|
{
|
||||||
|
$this->runInCoroutine(function (): void {
|
||||||
|
$test_user = $this->createTestUser();
|
||||||
|
|
||||||
|
$container = ApplicationContext::getContainer();
|
||||||
|
$command = new UserResetPasswordCommand($container);
|
||||||
|
$tester = new CommandTester($command);
|
||||||
|
$tester->execute([
|
||||||
|
'identifier' => $test_user->username,
|
||||||
|
'--password' => '12345',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(1, $tester->getStatusCode());
|
||||||
|
$this->assertStringContainsString('at least 6', $tester->getDisplay());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user