feat(manage): 团队管理支持管理员创建/批量导入员工账号(含部门、职位)

单个创建:邮箱/昵称/初始密码 + 可选首登改密、职位、部门(多选,选子部门自动补选上级,并加入对应部门群)。

批量导入:上传 Excel/CSV → 预览逐行校验 → 确认后导入。职位为模板第4列(选填,逐行解析校验),部门在预览表按行勾选后由底部设置部门到选中写入;导入按行返回结果(全成功关弹窗+成功提示;含失败留弹窗显示失败明细;仅 success>0 才刷新列表)。

后端:User::createByAdmin 选项数组化 + 校验助手 assertValidProfession/assertValidDepartments;importUsers 逐行 department/profession;UsersController createuser/import;UserImport/UserImportTemplate(含职位列)。

测试:tests/Feature/AdminCreateUserTest、tests/Unit/UserImportParseTest。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kuaifan
2026-06-01 01:26:34 +00:00
parent aa2e0acaba
commit 20b5daba50
12 changed files with 1297 additions and 2 deletions

View File

@@ -0,0 +1,137 @@
<?php
namespace Tests\Unit;
use App\Models\User;
use Tests\TestCase;
class UserImportParseTest extends TestCase
{
public function test_parse_skips_header_and_empty_rows()
{
$sheet = [
['邮箱', '昵称', '初始密码'], // 表头,应跳过
['a@test.local', '张三', 'Abc123456'],
['', '', ''], // 空行,应跳过
['b@test.local', '李四', 'Xyz123456'],
];
$rows = User::parseImportRows($sheet);
$this->assertCount(2, $rows);
$this->assertSame('a@test.local', $rows[0]['email']);
$this->assertSame('张三', $rows[0]['nickname']);
$this->assertSame('Abc123456', $rows[0]['password']);
$this->assertSame(2, $rows[0]['line']);
$this->assertSame(4, $rows[1]['line']);
}
public function test_parse_trims_cells()
{
$sheet = [
['邮箱', '昵称', '初始密码'],
[' a@test.local ', ' 张三 ', ' Abc123456 '],
];
$rows = User::parseImportRows($sheet);
$this->assertSame('a@test.local', $rows[0]['email']);
$this->assertSame('张三', $rows[0]['nickname']);
$this->assertSame('Abc123456', $rows[0]['password']);
}
public function test_validate_passes_for_valid_row()
{
$row = ['email' => 'ok@test.local', 'nickname' => '张三', 'password' => 'Abc123456'];
$this->assertNull(User::validateImportRow($row));
}
public function test_validate_requires_all_fields()
{
$this->assertSame('邮箱、昵称、初始密码均为必填', User::validateImportRow(['email' => '', 'nickname' => '张三', 'password' => 'Abc123456']));
$this->assertSame('邮箱、昵称、初始密码均为必填', User::validateImportRow(['email' => 'a@test.local', 'nickname' => '', 'password' => 'Abc123456']));
$this->assertSame('邮箱、昵称、初始密码均为必填', User::validateImportRow(['email' => 'a@test.local', 'nickname' => '张三', 'password' => '']));
}
public function test_validate_rejects_bad_email()
{
$this->assertSame('邮箱格式不正确', User::validateImportRow(['email' => 'not-an-email', 'nickname' => '张三', 'password' => 'Abc123456']));
}
public function test_validate_rejects_bad_nickname_length()
{
$this->assertSame('昵称需为2-20个字', User::validateImportRow(['email' => 'a@test.local', 'nickname' => '王', 'password' => 'Abc123456']));
$this->assertSame('昵称需为2-20个字', User::validateImportRow(['email' => 'a@test.local', 'nickname' => str_repeat('字', 21), 'password' => 'Abc123456']));
}
public function test_validate_rejects_short_password()
{
$this->assertNotNull(User::validateImportRow(['email' => 'a@test.local', 'nickname' => '张三', 'password' => '123']));
}
public function test_assert_valid_profession_passes_for_empty_and_normal()
{
// 空职位允许(可选字段),2/20 字边界与正常值允许;不抛异常即通过
User::assertValidProfession('');
User::assertValidProfession('工程'); // 恰好 2 字
User::assertValidProfession('工程师');
User::assertValidProfession(str_repeat('字', 20)); // 恰好 20 字
$this->assertTrue(true);
}
public function test_assert_valid_profession_rejects_too_short()
{
$this->expectException(\App\Exceptions\ApiException::class);
$this->expectExceptionMessage('职位/职称不可以少于2个字');
User::assertValidProfession('A');
}
public function test_assert_valid_profession_rejects_too_long()
{
$this->expectException(\App\Exceptions\ApiException::class);
$this->expectExceptionMessage('职位/职称最多只能设置20个字');
User::assertValidProfession(str_repeat('字', 21));
}
public function test_assert_valid_departments_normalizes_ids()
{
// 空/非数组 → 返回空数组
$this->assertSame([], User::assertValidDepartments([]));
$this->assertSame([], User::assertValidDepartments('not-array'));
// 去重 + 转 int + 过滤非正数(这些路径不查库)
$this->assertSame([3, 5], User::assertValidDepartments(['3', 3, 5, 0, -1]));
}
public function test_assert_valid_departments_rejects_over_limit()
{
// 超过 10 个(count 校验在查库之前)→ 抛异常
$this->expectException(\App\Exceptions\ApiException::class);
$this->expectExceptionMessage('最多只可加入10个部门');
User::assertValidDepartments(range(1, 11));
}
public function test_parse_reads_profession_column()
{
$sheet = [
['邮箱', '昵称', '初始密码', '职位'],
['a@test.local', '张三', 'Abc123456', '工程师'],
['b@test.local', '李四', 'Xyz123456'], // 无职位列 → profession 为空
];
$rows = User::parseImportRows($sheet);
$this->assertCount(2, $rows);
$this->assertSame('工程师', $rows[0]['profession']);
$this->assertSame('', $rows[1]['profession']);
}
public function test_validate_passes_for_empty_profession()
{
$row = ['email' => 'a@test.local', 'nickname' => '张三', 'password' => 'Abc123456', 'profession' => ''];
$this->assertNull(User::validateImportRow($row));
}
public function test_validate_rejects_bad_profession()
{
$row = ['email' => 'a@test.local', 'nickname' => '张三', 'password' => 'Abc123456', 'profession' => 'A'];
$this->assertSame('职位/职称不可以少于2个字', User::validateImportRow($row));
}
}