diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php
index 8479b4a27..9228d27e0 100755
--- a/app/Http/Controllers/Api/UsersController.php
+++ b/app/Http/Controllers/Api/UsersController.php
@@ -1094,6 +1094,8 @@ class UsersController extends AbstractController
* - clearadmin 取消管理员
* - settemp 设为临时帐号
* - cleartemp 取消临时身份(取消临时帐号)
+ * - setverity 标记邮箱为已认证
+ * - clearverity 标记邮箱为未认证
* - checkin_macs 修改自动签到mac地址(需要参数 checkin_macs)
* - checkin_face 修改签到人脸图片(需要参数 checkin_face)
* - department 修改部门(需要参数 department)
@@ -1157,6 +1159,16 @@ class UsersController extends AbstractController
$upArray['identity'] = array_diff($userInfo->identity, ['temp']);
break;
+ case 'setverity':
+ $msg = '设置成功';
+ $upArray['email_verity'] = 1;
+ break;
+
+ case 'clearverity':
+ $msg = '取消成功';
+ $upArray['email_verity'] = 0;
+ break;
+
case 'checkin_macs':
$list = is_array($data['checkin_macs']) ? $data['checkin_macs'] : [];
$array = [];
@@ -1354,6 +1366,7 @@ class UsersController extends AbstractController
* @apiParam {String} email 邮箱
* @apiParam {String} password 初始密码
* @apiParam {String} nickname 昵称
+ * @apiParam {Number} [email_verity] 是否标记邮箱为已认证(1是、0否,默认1)
* @apiParam {String} [profession] 职位/职称(可选,2-20字)
* @apiParam {Array} [department] 部门ID列表(可选,最多10个)
*/
@@ -1364,10 +1377,12 @@ class UsersController extends AbstractController
$password = trim(Request::input('password'));
$nickname = trim(Request::input('nickname'));
$changePass = intval(Request::input('changepass', 1)) === 1;
+ $emailVerity = intval(Request::input('email_verity', 1)) === 1;
$profession = trim((string)Request::input('profession', ''));
$department = Request::input('department', []);
$user = User::createByAdmin($email, $password, $nickname, [
'changePass' => $changePass,
+ 'emailVerity' => $emailVerity,
'profession' => $profession,
'department' => is_array($department) ? $department : [],
]);
@@ -1405,7 +1420,7 @@ class UsersController extends AbstractController
/**
* @api {post} api/users/import 批量导入用户(管理员)
*
- * @apiDescription 需要token身份(管理员)。提交预览确认后的行数据 rows(每行 {email,nickname,password,profession},可选 department[])进行创建
+ * @apiDescription 需要token身份(管理员)。提交预览确认后的行数据 rows(每行 {email,nickname,password,profession},可选 department[]、email_verity(1已认证/0未认证,默认0))进行创建
* @apiVersion 1.0.0
* @apiGroup users
* @apiName import
diff --git a/app/Models/User.php b/app/Models/User.php
index e1c79840a..6452c41d1 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -432,7 +432,7 @@ class User extends AbstractModel
* @param string $email
* @param string $password
* @param string $nickname
- * @param array $options changePass(bool,默认true) / department(int[]) / profession(string)
+ * @param array $options changePass(bool,默认true) / emailVerity(bool,默认false,标记邮箱已认证) / department(int[]) / profession(string)
* @return self
* @throws ApiException
*/
@@ -443,6 +443,7 @@ class User extends AbstractModel
throw new ApiException('昵称需为2-20个字');
}
$changePass = ($options['changePass'] ?? true) ? 1 : 0;
+ $emailVerity = ($options['emailVerity'] ?? false) ? 1 : 0;
$profession = trim((string)($options['profession'] ?? ''));
// 校验前置(reg 之前快速失败,且可在无 Swoole 环境单测)
self::assertValidProfession($profession);
@@ -454,6 +455,7 @@ class User extends AbstractModel
$user->identity = Base::arrayImplode(array_diff($user->identity, ['temp']));
}
$user->changepass = $changePass; // 复用现有首登强制改密机制
+ $user->email_verity = $emailVerity; // 管理员可在创建时直接标记邮箱认证状态
if ($profession !== '') {
$user->profession = $profession;
}
@@ -619,6 +621,7 @@ class User extends AbstractModel
try {
self::createByAdmin($row['email'], $row['password'], $row['nickname'], [
'changePass' => $changePass,
+ 'emailVerity' => !empty($row['email_verity']),
'department' => $row['department'] ?? [],
'profession' => $row['profession'] ?? '',
]);
@@ -692,6 +695,7 @@ class User extends AbstractModel
'nickname' => $row['nickname'] ?? '',
'password' => $row['password'] ?? '',
'profession' => $row['profession'] ?? '',
+ 'email_verity' => 1, // 默认标记为已认证,前端可在预览中按行调整
'status' => $ok ? 'ok' : 'error',
'reason' => $reason ?? '',
];
diff --git a/language/original-web.txt b/language/original-web.txt
index 9d15d1d59..e9629988b 100644
--- a/language/original-web.txt
+++ b/language/original-web.txt
@@ -1668,6 +1668,12 @@ WiFi签到延迟时长为±1分钟。
你确定将【(*)】设为管理员吗?
你确定取消【(*)】管理员身份吗?
+你确定将【(*)】的邮箱标记为已认证吗?
+你确定将【(*)】的邮箱标记为未认证吗?
+标记邮箱为已认证
+标记邮箱为未认证
+标记选中(*)项为已认证
+标记选中(*)项为未认证
你确定要取消任务时间吗?
更新子任务
diff --git a/resources/assets/js/pages/manage/components/CreateUserModal.vue b/resources/assets/js/pages/manage/components/CreateUserModal.vue
index ac5c4a16b..a883f33cd 100644
--- a/resources/assets/js/pages/manage/components/CreateUserModal.vue
+++ b/resources/assets/js/pages/manage/components/CreateUserModal.vue
@@ -7,12 +7,14 @@
@@ -61,14 +60,14 @@ export default {
return {
show: false,
loading: false,
- formData: {email: '', nickname: '', password: '', changepass: true, profession: '', department: []},
+ formData: {email: '', nickname: '', password: '', changepass: true, email_verity: true, profession: '', department: []},
}
},
watch: {
value(val) {
this.show = val;
if (val) {
- this.formData = {email: '', nickname: '', password: '', changepass: true, profession: '', department: []};
+ this.formData = {email: '', nickname: '', password: '', changepass: true, email_verity: true, profession: '', department: []};
}
},
show(val) {
@@ -115,7 +114,7 @@ export default {
this.show = false;
},
onSubmit() {
- const {email, nickname, password, changepass, profession, department} = this.formData;
+ const {email, nickname, password, changepass, email_verity, profession, department} = this.formData;
if (!email || !nickname || !password) {
$A.messageWarning('邮箱、昵称、初始密码均为必填');
return;
@@ -123,7 +122,7 @@ export default {
this.loading = true;
this.$store.dispatch("call", {
url: 'users/createuser',
- data: {email, nickname, password, changepass: changepass ? 1 : 0, profession, department},
+ data: {email, nickname, password, changepass: changepass ? 1 : 0, email_verity: email_verity ? 1 : 0, profession, department},
}).then(() => {
this.loading = false;
$A.messageSuccess('创建成功');
diff --git a/resources/assets/js/pages/manage/components/ImportUserModal.vue b/resources/assets/js/pages/manage/components/ImportUserModal.vue
index be8288114..09e44b539 100644
--- a/resources/assets/js/pages/manage/components/ImportUserModal.vue
+++ b/resources/assets/js/pages/manage/components/ImportUserModal.vue
@@ -38,6 +38,7 @@
@on-selection-change="onSelectionChange"/>
+ {{$L('所属部门')}}
-
+
+
+ {{$L('邮箱认证')}}
+
+ {{$L('标记选中(*)项为已认证', selectedRows.length)}}
+
+
+ {{$L('标记选中(*)项为未认证', selectedRows.length)}}
+
+
{{$L('员工首次登录需修改密码')}}
@@ -117,7 +128,22 @@ export default {
previewColumns: [
{type: 'selection', width: 50, align: 'center'},
{title: this.$L('行号'), key: 'line', width: 64, align: 'center'},
- {title: this.$L('邮箱'), minWidth: 150, render: (h, {row}) => h('AutoTip', row.email || '-')},
+ {
+ title: this.$L('邮箱'),
+ minWidth: 150,
+ render: (h, {row}) => {
+ // 列渲染发生在 Table 的上下文,scoped 样式不生效,故图标颜色/布局内联(与会员列表 $primary-color 一致)
+ const arr = [h('AutoTip', {style: {minWidth: '50px'}}, row.email || '-')];
+ if (row.email_verity && row.status === 'ok') {
+ arr.push(h('Icon', {
+ props: {type: 'md-mail'},
+ attrs: {title: this.$L('已邮箱认证')},
+ style: {color: '#84C56A', marginLeft: '6px', fontSize: '16px', flexShrink: 0},
+ }));
+ }
+ return h('div', {style: {display: 'flex', alignItems: 'center'}}, arr);
+ }
+ },
{title: this.$L('昵称'), width: 90, render: (h, {row}) => h('AutoTip', row.nickname || '-')},
{
title: this.$L('初始密码'),
@@ -254,6 +280,7 @@ export default {
const data = res.data;
(data.rows || []).forEach(row => {
this.$set(row, 'department', []); // 逐行部门,默认空
+ this.$set(row, 'email_verity', row.email_verity ? 1 : 0); // 逐行邮箱认证,默认已认证
if (row.status !== 'ok') {
this.$set(row, '_disabled', true); // 错误行不可勾选
}
@@ -280,19 +307,32 @@ export default {
}
});
},
+ onApplyVerity(verity) {
+ if (this.selectedRows.length === 0) {
+ return;
+ }
+ // 与 onApplyDepartment 一致:按唯一 line 匹配回 preview.rows 的原始对象再写入
+ const selectedLines = new Set(this.selectedRows.map(row => row.line));
+ (this.preview && this.preview.rows ? this.preview.rows : []).forEach(row => {
+ if (selectedLines.has(row.line)) {
+ this.$set(row, 'email_verity', verity ? 1 : 0);
+ }
+ });
+ },
onConfirmImport() {
if (!this.preview || this.preview.valid === 0) {
return;
}
const rows = this.preview.rows
.filter(row => row.status === 'ok')
- .map(({line, email, nickname, password, profession, department}) => ({
+ .map(({line, email, nickname, password, profession, department, email_verity}) => ({
line,
email,
nickname,
password,
profession: profession || '',
department: Array.isArray(department) ? department : [],
+ email_verity: email_verity ? 1 : 0,
}));
this.importing = true;
this.$store.dispatch("call", {
@@ -336,15 +376,26 @@ export default {
.import-tip { color: #808695; margin-bottom: 12px; }
.import-actions { display: flex; gap: 12px; align-items: center; }
.import-option { margin-top: 12px; }
+ .import-batch-label {
+ flex-shrink: 0;
+ min-width: 64px;
+ color: #515a6e;
+ }
.import-setdept {
display: flex;
- align-items: flex-start;
+ align-items: center;
gap: 8px;
margin-top: 12px;
.import-setdept-select {
width: auto;
}
}
+ .import-setverity {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 12px;
+ }
.import-preview { margin-top: 16px; }
.import-result { margin-top: 16px; }
diff --git a/resources/assets/js/pages/manage/components/TeamManagement.vue b/resources/assets/js/pages/manage/components/TeamManagement.vue
index 856864377..767e46e7a 100644
--- a/resources/assets/js/pages/manage/components/TeamManagement.vue
+++ b/resources/assets/js/pages/manage/components/TeamManagement.vue
@@ -538,7 +538,7 @@ export default {
align: 'center',
width: 100,
render: (h, params) => {
- const identity = params.row.identity;
+ const {identity, email_verity} = params.row;
const dropdownItems = [];
dropdownItems.push(h('EDropdownItem', {
props: {
@@ -573,6 +573,20 @@ export default {
},
}, [h('div', this.$L('设为临时帐号'))]));
}
+ // 邮箱认证状态
+ if (email_verity) {
+ dropdownItems.push(h('EDropdownItem', {
+ props: {
+ command: 'clearverity',
+ },
+ }, [h('div', this.$L('标记邮箱为未认证'))]));
+ } else {
+ dropdownItems.push(h('EDropdownItem', {
+ props: {
+ command: 'setverity',
+ },
+ }, [h('div', this.$L('标记邮箱为已认证'))]));
+ }
// 编辑用户信息
dropdownItems.push(h('EDropdownItem', {
props: {
@@ -961,6 +975,32 @@ export default {
});
break;
+ case 'setverity':
+ $A.modalConfirm({
+ content: `你确定将【ID:${row.userid}, ${row.nickname}】的邮箱标记为已认证吗?`,
+ loading: true,
+ onOk: () => {
+ return this.operationUser({
+ userid: row.userid,
+ type: name
+ });
+ }
+ });
+ break;
+
+ case 'clearverity':
+ $A.modalConfirm({
+ content: `你确定将【ID:${row.userid}, ${row.nickname}】的邮箱标记为未认证吗?`,
+ loading: true,
+ onOk: () => {
+ return this.operationUser({
+ userid: row.userid,
+ type: name
+ });
+ }
+ });
+ break;
+
case 'setdisable':
this.disableData = {
type: 'setdisable',
diff --git a/tests/Feature/AdminCreateUserTest.php b/tests/Feature/AdminCreateUserTest.php
index 943c39cc9..155db7827 100644
--- a/tests/Feature/AdminCreateUserTest.php
+++ b/tests/Feature/AdminCreateUserTest.php
@@ -141,6 +141,20 @@ class AdminCreateUserTest extends TestCase
$this->assertSame(0, User::whereEmail('newp1@test.local')->count());
}
+ public function test_import_preview_defaults_email_verity_to_verified()
+ {
+ // 预览默认逐行标记为已认证(email_verity=1),前端可再按行调整
+ $rows = [
+ ['line' => 2, 'email' => 'verity1@test.local', 'nickname' => '张三', 'password' => 'Abc123456'],
+ ['line' => 3, 'email' => 'bad', 'nickname' => '李四', 'password' => 'Abc123456'], // 错误行同样带默认值
+ ];
+
+ $preview = User::importPreview($rows);
+
+ $this->assertSame(1, $preview['rows'][0]['email_verity']);
+ $this->assertSame(1, $preview['rows'][1]['email_verity']);
+ }
+
public function test_import_collects_all_invalid_rows_without_creating()
{
// 全部非法 → 不触发 createByAdmin/SO,可在无 Swoole 环境稳定运行