Compare commits
3 Commits
8291a89898
...
7d683f3e65
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d683f3e65 | |||
| 772f3f45f4 | |||
| d651d19e86 |
@ -174,6 +174,49 @@ def create_user():
|
||||
return jsonify({'msg': str(e)}), 400
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 批量创建用户
|
||||
# ==============================================================================
|
||||
@auth_bp.route('/user/batch', methods=['POST'])
|
||||
@jwt_required()
|
||||
def batch_create_user():
|
||||
try:
|
||||
data_list = request.get_json()
|
||||
if not data_list or not isinstance(data_list, list):
|
||||
return jsonify({'msg': '请求数据必须是用户数组'}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
for data in data_list:
|
||||
if 'system_user:*' not in user_permissions:
|
||||
field_to_perm = {
|
||||
'cn_name': 'system_user:username',
|
||||
'username': 'system_user:username',
|
||||
'password': 'system_user:password',
|
||||
'department': 'system_user:department',
|
||||
'role': 'system_user:role',
|
||||
'email': 'system_user:email',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if field == 'password':
|
||||
if 'system_user:operation' not in user_permissions:
|
||||
data.pop(field, None)
|
||||
continue
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
claims = get_jwt()
|
||||
operator_role = claims.get('role')
|
||||
|
||||
results = AuthService.batch_create_users(data_list, operator_role)
|
||||
return jsonify({'msg': '批量处理完成', 'data': results}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Batch User Create Failed: {str(e)}")
|
||||
return jsonify({'msg': str(e)}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 更新用户(管理员)
|
||||
# ==============================================================================
|
||||
@ -371,3 +414,55 @@ def change_my_password():
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Change Password Failed: {str(e)}")
|
||||
return jsonify({'msg': f'密码修改失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 自我更新邮箱
|
||||
# ==============================================================================
|
||||
@auth_bp.route('/me/email', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_my_email():
|
||||
"""
|
||||
自我更新邮箱接口
|
||||
- 仅更新 email 字段,与密码修改完全隔离
|
||||
- 防止后端意外清空用户密码
|
||||
"""
|
||||
try:
|
||||
from app.models.system import SysUser
|
||||
|
||||
user_id = get_jwt_identity()
|
||||
|
||||
# 超级管理员(user_id=0)不允许修改邮箱
|
||||
if user_id == 0:
|
||||
return jsonify({'msg': '超级管理员邮箱由系统管理员管理'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'msg': '无效的请求数据'}), 400
|
||||
|
||||
email = data.get('email')
|
||||
if not email:
|
||||
return jsonify({'msg': '邮箱不能为空'}), 400
|
||||
|
||||
# 简单的邮箱格式校验
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
|
||||
return jsonify({'msg': '邮箱格式不正确'}), 400
|
||||
|
||||
user = SysUser.query.get(user_id)
|
||||
if not user:
|
||||
return jsonify({'msg': '用户不存在'}), 404
|
||||
|
||||
# 检查邮箱是否已被其他用户使用
|
||||
existing = SysUser.query.filter(SysUser.email == email, SysUser.id != user_id).first()
|
||||
if existing:
|
||||
return jsonify({'msg': '该邮箱已被其他用户使用'}), 400
|
||||
|
||||
user.email = email
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'msg': '邮箱更新成功'}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Update Email Failed: {str(e)}")
|
||||
return jsonify({'msg': f'邮箱更新失败: {str(e)}'}), 500
|
||||
|
||||
@ -205,6 +205,16 @@ class AuthService:
|
||||
if not cn_name or not pinyin_base:
|
||||
raise Exception("姓名和账号不能为空")
|
||||
|
||||
# 后端兜底正则校验:允许中英数,禁止纯数字,无特殊字符
|
||||
import re
|
||||
name_pattern = re.compile(r'^(?!\d+$)[a-zA-Z0-9\u4e00-\u9fa5]+$')
|
||||
|
||||
if not name_pattern.match(cn_name):
|
||||
raise Exception("姓名格式错误:仅支持中英文和数字,不能为纯数字,且不支持特殊字符")
|
||||
|
||||
if not name_pattern.match(pinyin_base):
|
||||
raise Exception("账号格式错误:仅支持中英文和数字,不能为纯数字,且不支持特殊字符")
|
||||
|
||||
role_raw = data.get('role')
|
||||
role = role_raw.upper() if role_raw else None
|
||||
|
||||
@ -220,7 +230,7 @@ class AuthService:
|
||||
if operator_role_upper == UserRole.SUPERVISOR and role == UserRole.SUPER_ADMIN:
|
||||
raise Exception("权限不足:主管无法创建超级管理员")
|
||||
|
||||
email = data.get('email', '')
|
||||
email = data.get('email', '') or None # 空字符串转 None,避免 unique 冲突
|
||||
if email and SysUser.query.filter_by(email=email).first():
|
||||
raise Exception("邮箱已被使用")
|
||||
|
||||
@ -260,6 +270,29 @@ class AuthService:
|
||||
# 返回时,最好把生成的ID告诉前端
|
||||
return new_user.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def batch_create_users(data_list, operator_role):
|
||||
"""
|
||||
批量创建新用户。复用 create_user 的核心防重逻辑。
|
||||
"""
|
||||
results = []
|
||||
for data in data_list:
|
||||
try:
|
||||
# 复用单条创建逻辑,它自带张三/zhangsan1的防重机制
|
||||
new_user_dict = AuthService.create_user(data, operator_role)
|
||||
results.append({
|
||||
"cn_name": data.get('cn_name'),
|
||||
"account_id": new_user_dict.get('account_id'),
|
||||
"status": "success"
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"cn_name": data.get('cn_name'),
|
||||
"error": str(e),
|
||||
"status": "fail"
|
||||
})
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def update_user(user_id, data, operator_role):
|
||||
"""
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { InfoFilled, SwitchButton, UserFilled, Lock, User, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMyProfile, changeMyPassword } from '@/api/auth'
|
||||
import { getMyProfile, changeMyPassword, updateMyEmail } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -34,13 +34,15 @@ interface ProfileData {
|
||||
username: string
|
||||
display_name: string
|
||||
department: string
|
||||
email: string
|
||||
}
|
||||
|
||||
const profileForm = ref<ProfileData>({
|
||||
id: 0,
|
||||
username: '',
|
||||
display_name: '',
|
||||
department: ''
|
||||
department: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const passwordForm = ref({
|
||||
@ -50,6 +52,62 @@ const passwordForm = ref({
|
||||
|
||||
const passwordFormRef = ref()
|
||||
|
||||
// ================================================================
|
||||
// 绑定/修改邮箱
|
||||
// ================================================================
|
||||
const emailDialogVisible = ref(false)
|
||||
const emailLoading = ref(false)
|
||||
const emailFormRef = ref()
|
||||
|
||||
interface EmailForm {
|
||||
email: string
|
||||
}
|
||||
|
||||
const emailForm = ref<EmailForm>({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const emailRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
|
||||
]
|
||||
}
|
||||
|
||||
// 打开邮箱弹窗
|
||||
const openEmailDialog = () => {
|
||||
emailForm.value.email = profileForm.value.email || ''
|
||||
emailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交邮箱修改
|
||||
const submitEmailUpdate = async () => {
|
||||
const formRef = emailFormRef.value
|
||||
if (!formRef) return
|
||||
|
||||
await formRef.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
emailLoading.value = true
|
||||
try {
|
||||
await updateMyEmail({ email: emailForm.value.email })
|
||||
ElMessage.success('邮箱绑定成功')
|
||||
emailDialogVisible.value = false
|
||||
// 刷新个人资料
|
||||
openProfileDialog()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.msg || e?.message || '绑定失败')
|
||||
} finally {
|
||||
emailLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetEmailForm = () => {
|
||||
emailFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 打开个人中心弹窗
|
||||
const openProfileDialog = async () => {
|
||||
profileDialogVisible.value = true
|
||||
@ -210,6 +268,12 @@ const handleLogout = () => {
|
||||
<!-- 【严格脱敏】系统角色字段已移除,不在此展示 -->
|
||||
</div>
|
||||
|
||||
<div style="margin: 16px 0; text-align: center;">
|
||||
<el-button type="primary" plain @click="openEmailDialog">
|
||||
绑定/修改邮箱
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider>
|
||||
<el-icon><Lock /></el-icon> 修改密码
|
||||
</el-divider>
|
||||
@ -260,6 +324,19 @@ const handleLogout = () => {
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 绑定/修改邮箱弹窗 -->
|
||||
<el-dialog v-model="emailDialogVisible" title="绑定/修改邮箱" width="400px" @close="resetEmailForm">
|
||||
<el-form :model="emailForm" :rules="emailRules" ref="emailFormRef" label-width="80px">
|
||||
<el-form-item label="新邮箱" prop="email">
|
||||
<el-input v-model="emailForm.email" placeholder="请输入有效邮箱地址" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="emailDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="emailLoading" @click="submitEmailUpdate">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -67,3 +67,21 @@ export function changeMyPassword(data: { new_password: string; confirm_password:
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 【新增】自我更新邮箱(与密码修改完全隔离)
|
||||
export function updateMyEmail(data: { email: string }) {
|
||||
return request({
|
||||
url: '/v1/auth/me/email',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 【新增】批量创建用户
|
||||
export function batchCreateUser(data: any[]) {
|
||||
return request({
|
||||
url: '/v1/auth/user/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
@ -4,9 +4,14 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span style="font-weight: bold;">员工账号管理</span>
|
||||
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="handleCreate">
|
||||
+ 新增员工
|
||||
</el-button>
|
||||
<div>
|
||||
<el-button v-if="userStore.hasPermission('system_user:operation')" type="success" @click="batchDialogVisible = true">
|
||||
批量新增
|
||||
</el-button>
|
||||
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="handleCreate">
|
||||
+ 新增员工
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -138,12 +143,52 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量新增弹窗 -->
|
||||
<el-dialog v-model="batchDialogVisible" title="批量新增员工" width="600px" destroy-on-close @close="batchForm.namesText = ''">
|
||||
<el-form :model="batchForm" label-width="100px">
|
||||
<el-form-item label="所属部门" required>
|
||||
<el-select v-model="batchForm.department" style="width: 100%" placeholder="请选择部门">
|
||||
<el-option v-for="d in departmentOptions" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统角色" required>
|
||||
<el-select v-model="batchForm.role" style="width: 100%" placeholder="请选择角色">
|
||||
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="员工名单" required>
|
||||
<el-input type="textarea" v-model="batchForm.namesText" :rows="8" placeholder="请输入真实姓名,每行一个。密码默认统一为 123456" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchSubmitting" @click="handleBatchSubmit">确认创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量创建结果弹窗 -->
|
||||
<el-dialog v-model="batchResultVisible" title="批量创建结果" width="600px">
|
||||
<div style="margin-bottom: 10px; color: #67C23A; font-weight: bold;">请复制以下账号分发给员工:</div>
|
||||
<el-table :data="batchResults" border height="400px">
|
||||
<el-table-column prop="cn_name" label="姓名" width="150" />
|
||||
<el-table-column label="生成账号">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status === 'success'" style="color: #409EFF; font-weight: bold;">{{ row.account_id }}</span>
|
||||
<span v-else style="color: #F56C6C">{{ row.error }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="batchResultVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
import { createUser, updateUser, getUserList, deleteUser } from '@/api/auth'
|
||||
import { createUser, updateUser, getUserList, deleteUser, batchCreateUser } from '@/api/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
|
||||
@ -199,6 +244,17 @@ const form = reactive({
|
||||
email: ''
|
||||
})
|
||||
|
||||
// 批量新增相关状态
|
||||
const batchDialogVisible = ref(false)
|
||||
const batchResultVisible = ref(false)
|
||||
const batchSubmitting = ref(false)
|
||||
const batchForm = reactive({
|
||||
namesText: '',
|
||||
department: '',
|
||||
role: ''
|
||||
})
|
||||
const batchResults = ref<any[]>([])
|
||||
|
||||
// ★ 监听中文输入,自动转拼音
|
||||
const handleNameInput = (val: string) => {
|
||||
if (isEdit.value) return // 编辑模式下不联动
|
||||
@ -246,10 +302,30 @@ const roleOptions = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
// 自定义校验:仅支持中英文、数字,禁止纯数字,禁止特殊字符
|
||||
const validateNameStrict = (rule: any, value: string, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error('该字段不能为空'));
|
||||
return;
|
||||
}
|
||||
const reg = /^(?!\d+$)[a-zA-Z0-9\u4e00-\u9fa5]+$/;
|
||||
if (!reg.test(value)) {
|
||||
callback(new Error('仅支持中英文和数字,不能为纯数字,且不支持特殊字符'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const rules = computed(() => {
|
||||
const commonRules: any = {
|
||||
cn_name: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '账号不能为空', trigger: 'blur' }],
|
||||
cn_name: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||
{ validator: validateNameStrict, trigger: 'blur' }
|
||||
],
|
||||
username: [
|
||||
{ required: true, message: '账号不能为空', trigger: 'blur' },
|
||||
{ validator: validateNameStrict, trigger: 'blur' }
|
||||
],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
department: [{ required: true, message: '请输入或选择部门', trigger: ['blur', 'change'] }],
|
||||
email: [
|
||||
@ -370,6 +446,43 @@ const onSubmit = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 批量提交逻辑
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchForm.department || !batchForm.role || !batchForm.namesText.trim()) {
|
||||
return ElMessage.warning('请填写完整部门、角色及员工名单')
|
||||
}
|
||||
|
||||
const names = batchForm.namesText.split('\n').map(n => n.trim()).filter(n => n)
|
||||
if (names.length === 0) return ElMessage.warning('未能识别到有效姓名')
|
||||
|
||||
const payload = names.map(name => {
|
||||
const pinyinStr = pinyin(name, { toneType: 'none', type: 'array' }).join('').toLowerCase()
|
||||
return {
|
||||
cn_name: name,
|
||||
username: pinyinStr, // 拼音基础串,后端会自动防重
|
||||
password: '123456',
|
||||
department: batchForm.department,
|
||||
role: batchForm.role,
|
||||
email: ''
|
||||
}
|
||||
})
|
||||
|
||||
batchSubmitting.value = true
|
||||
try {
|
||||
const res: any = await batchCreateUser(payload)
|
||||
if (res.code === 200 || res.msg === '批量处理完成') {
|
||||
batchResults.value = res.data
|
||||
batchDialogVisible.value = false
|
||||
batchResultVisible.value = true
|
||||
getList() // 刷新底层列表
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('批量创建遇到错误')
|
||||
} finally {
|
||||
batchSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
if (!formRef.value) return
|
||||
formRef.value.resetFields()
|
||||
|
||||
Reference in New Issue
Block a user