feat: improve error handling and code quality

后端改进:
- 添加统一异常处理系统 (exceptions.py, response.py)
- 实现自定义异常类 (ValidationError, AuthorizationError, ResourceNotFoundError, BusinessLogicError)
- 配置全局异常处理器,统一 API 错误响应格式
- 迁移业务逻辑错误到自定义异常 (users.py, auth.py)
- 添加 SQL LIKE 通配符转义,防止通配符滥用
- 使用 EmailStr 进行邮箱格式验证
- 移除敏感字段暴露 (jwt_sub)

前端改进:
- 配置 ESLint 9 (flat config) 和 Prettier
- 修复所有 ESLint 错误和警告
- 移除未使用的变量和导入
- 为组件添加 PropTypes 默认值
- 统一代码格式和风格
This commit is contained in:
2026-01-03 19:01:15 +08:00
parent 523da50123
commit 5cdc8b2144
57 changed files with 4623 additions and 2754 deletions
+158 -156
View File
@@ -22,13 +22,13 @@
</template>
<!-- Tab 切换 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tabs v-model:active-key="activeTab" @change="handleTabChange">
<!-- 待审批用户 Tab -->
<a-tab-pane key="pending" tab="待审批用户">
<!-- 桌面端表格 -->
<a-table
v-if="!isMobile"
:dataSource="pendingUsers"
:data-source="pendingUsers"
:columns="pendingColumns"
:loading="loading"
:row-key="record => record.id"
@@ -44,9 +44,7 @@
<a-button type="primary" size="small" @click="handleApprove(record)">
通过
</a-button>
<a-button danger size="small" @click="handleReject(record)">
拒绝
</a-button>
<a-button danger size="small" @click="handleReject(record)"> 拒绝 </a-button>
</a-space>
</template>
</template>
@@ -59,10 +57,14 @@
<a-descriptions-item label="ID">{{ user.id }}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ user.alias }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ user.email || '-' }}</a-descriptions-item>
<a-descriptions-item label="注册时间">{{ formatDateTime(user.created_at) }}</a-descriptions-item>
<a-descriptions-item label="注册时间">{{
formatDateTime(user.created_at)
}}</a-descriptions-item>
</a-descriptions>
<a-space class="mt-3" style="width: 100%">
<a-button type="primary" size="small" block @click="handleApprove(user)">通过</a-button>
<a-button type="primary" size="small" block @click="handleApprove(user)"
>通过</a-button
>
<a-button danger size="small" block @click="handleReject(user)">拒绝</a-button>
</a-space>
</a-card>
@@ -75,7 +77,7 @@
<!-- 桌面端表格 -->
<a-table
v-if="!isMobile"
:dataSource="userStore.users"
:data-source="userStore.users"
:columns="allColumns"
:loading="loading"
:row-key="record => record.id"
@@ -95,7 +97,11 @@
</a-tag>
</template>
<template v-else-if="column.key === 'jwt_exp'">
{{ record.jwt_exp && record.jwt_exp !== '0' ? formatDateTime(parseInt(record.jwt_exp) * 1000) : '-' }}
{{
record.jwt_exp && record.jwt_exp !== '0'
? formatDateTime(parseInt(record.jwt_exp) * 1000)
: '-'
}}
</template>
<template v-else-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
@@ -105,9 +111,7 @@
<a-button type="primary" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button danger size="small" @click="handleDelete(record)">
删除
</a-button>
<a-button danger size="small" @click="handleDelete(record)"> 删除 </a-button>
</a-space>
</template>
</template>
@@ -115,7 +119,12 @@
<!-- 移动端卡片视图 -->
<a-space v-else direction="vertical" :size="16" style="width: 100%">
<a-card v-for="user in userStore.users" :key="user.id" size="small" :loading="loading">
<a-card
v-for="user in userStore.users"
:key="user.id"
size="small"
:loading="loading"
>
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="ID">{{ user.id }}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ user.alias }}</a-descriptions-item>
@@ -131,32 +140,38 @@
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="Token过期">
{{ user.jwt_exp && user.jwt_exp !== '0' ? formatDateTime(parseInt(user.jwt_exp) * 1000) : '-' }}
{{
user.jwt_exp && user.jwt_exp !== '0'
? formatDateTime(parseInt(user.jwt_exp) * 1000)
: '-'
}}
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDateTime(user.created_at) }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{
formatDateTime(user.created_at)
}}</a-descriptions-item>
</a-descriptions>
<a-space class="mt-3" style="width: 100%">
<a-button type="primary" size="small" block @click="handleEdit(user)">编辑</a-button>
<a-button type="primary" size="small" block @click="handleEdit(user)"
>编辑</a-button
>
<a-button danger size="small" block @click="handleDelete(user)">删除</a-button>
</a-space>
</a-card>
</a-space>
<!-- 批量操作 -->
<div class="batch-actions" v-if="selectedUsers.length > 0">
<div v-if="selectedUsers.length > 0" class="batch-actions">
<a-alert
:message="`已选择 ${selectedUsers.length} 个用户`"
type="info"
:closable="false"
>
<template #description>
<a-space style="margin-top: 10px;">
<a-space style="margin-top: 10px">
<a-button type="primary" size="small" @click="handleBatchApprove">
批量审批
</a-button>
<a-button danger size="small" @click="handleBatchDelete">
批量删除
</a-button>
<a-button danger size="small" @click="handleBatchDelete"> 批量删除 </a-button>
</a-space>
</template>
</a-alert>
@@ -167,17 +182,12 @@
<!-- 创建/编辑用户对话框 -->
<a-modal
:title="dialogMode === 'create' ? '创建用户' : '编辑用户'"
v-model:open="dialogVisible"
:title="dialogMode === 'create' ? '创建用户' : '编辑用户'"
:width="isMobile ? '100%' : 600"
:style="isMobile ? { top: 0, maxWidth: '100vw' } : {}"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="用户名" name="alias">
<a-input v-model:value="formData.alias" placeholder="请输入用户名" />
</a-form-item>
@@ -203,14 +213,12 @@
v-model:value="formData.password"
:placeholder="dialogMode === 'create' ? '请输入密码' : '留空则不修改密码'"
/>
<span class="form-hint" v-if="dialogMode === 'edit'">
留空则不修改密码
</span>
<span v-if="dialogMode === 'edit'" class="form-hint"> 留空则不修改密码 </span>
</a-form-item>
<a-form-item label="重置密码" v-if="dialogMode === 'edit'">
<a-form-item v-if="dialogMode === 'edit'" label="重置密码">
<a-switch v-model:checked="formData.reset_password" />
<span class="form-hint-danger" v-if="formData.reset_password">
<span v-if="formData.reset_password" class="form-hint-danger">
⚠️ 将重置为默认密码
</span>
</a-form-item>
@@ -218,9 +226,7 @@
<template #footer>
<a-button @click="dialogVisible = false">取消</a-button>
<a-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</a-button>
<a-button type="primary" :loading="submitting" @click="handleSubmit"> 确定 </a-button>
</template>
</a-modal>
</div>
@@ -228,31 +234,29 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { UserOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import Layout from '@/components/Layout.vue'
import { useBreakpoint } from '@/composables/useBreakpoint'
import { useUserStore } from '@/stores/user'
import { useAdminStore } from '@/stores/admin'
import { adminAPI } from '@/api/index'
import { ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { UserOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import Layout from '@/components/Layout.vue';
import { useBreakpoint } from '@/composables/useBreakpoint';
import { useUserStore } from '@/stores/user';
import { adminAPI } from '@/api/index';
const userStore = useUserStore()
const adminStore = useAdminStore()
const { isMobile } = useBreakpoint()
const userStore = useUserStore();
const { isMobile } = useBreakpoint();
// 状态
const loading = ref(false)
const activeTab = ref('all') // 默认展示所有用户
const pendingUsers = ref([])
const selectedUsers = ref([])
const selectedRowKeys = ref([])
const dialogVisible = ref(false)
const dialogMode = ref('create')
const submitting = ref(false)
const loading = ref(false);
const activeTab = ref('all'); // 默认展示所有用户
const pendingUsers = ref([]);
const selectedUsers = ref([]);
const selectedRowKeys = ref([]);
const dialogVisible = ref(false);
const dialogMode = ref('create');
const submitting = ref(false);
// 表单
const formRef = ref(null)
const formRef = ref(null);
const formData = ref({
alias: '',
role: 'user',
@@ -260,7 +264,7 @@ const formData = ref({
email: '',
password: '',
reset_password: false,
})
});
// 表单验证规则
const formRules = {
@@ -269,15 +273,13 @@ const formRules = {
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
],
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
],
}
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
};
// 时间格式化
const formatDateTime = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
const formatDateTime = timestamp => {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
@@ -285,8 +287,8 @@ const formatDateTime = (timestamp) => {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
});
};
// 待审批用户表格列
const pendingColumns = [
@@ -295,7 +297,7 @@ const pendingColumns = [
{ title: '邮箱', dataIndex: 'email', key: 'email', ellipsis: true },
{ title: '注册时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
{ title: '操作', key: 'actions', width: 200, fixed: 'right' },
]
];
// 所有用户表格列
const allColumns = [
@@ -307,40 +309,40 @@ const allColumns = [
{ title: 'Token 过期时间', dataIndex: 'jwt_exp', key: 'jwt_exp', width: 180 },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
{ title: '操作', key: 'actions', width: 200, fixed: 'right' },
]
];
// 行选择配置
const rowSelection = {
selectedRowKeys: selectedRowKeys,
onChange: (keys, rows) => {
selectedRowKeys.value = keys
selectedUsers.value = rows
selectedRowKeys.value = keys;
selectedUsers.value = rows;
},
}
};
// 获取待审批用户
const fetchPendingUsers = async () => {
loading.value = true
loading.value = true;
try {
pendingUsers.value = await adminAPI.getPendingUsers()
pendingUsers.value = await adminAPI.getPendingUsers();
} catch (error) {
message.error(error.message || '获取待审批用户失败')
message.error(error.message || '获取待审批用户失败');
} finally {
loading.value = false
loading.value = false;
}
}
};
// Tab 切换
const handleTabChange = (tab) => {
const handleTabChange = tab => {
if (tab === 'pending') {
fetchPendingUsers()
fetchPendingUsers();
} else {
handleRefresh()
handleRefresh();
}
}
};
// 审批通过用户
const handleApprove = async (user) => {
const handleApprove = async user => {
Modal.confirm({
title: '审批确认',
content: `确认通过用户 "${user.alias}" 的审批吗?`,
@@ -348,18 +350,18 @@ const handleApprove = async (user) => {
cancelText: '取消',
onOk: async () => {
try {
await adminAPI.approveUser(user.id)
message.success('审批成功')
fetchPendingUsers()
await adminAPI.approveUser(user.id);
message.success('审批成功');
fetchPendingUsers();
} catch (error) {
message.error(error.message || '审批失败')
message.error(error.message || '审批失败');
}
},
})
}
});
};
// 拒绝用户
const handleReject = async (user) => {
const handleReject = async user => {
Modal.confirm({
title: '拒绝确认',
content: `确认拒绝用户 "${user.alias}" 的申请吗?拒绝后将删除该用户。`,
@@ -368,36 +370,36 @@ const handleReject = async (user) => {
okType: 'danger',
onOk: async () => {
try {
await adminAPI.rejectUser(user.id)
message.success('已拒绝并删除用户')
fetchPendingUsers()
await adminAPI.rejectUser(user.id);
message.success('已拒绝并删除用户');
fetchPendingUsers();
} catch (error) {
message.error(error.message || '操作失败')
message.error(error.message || '操作失败');
}
},
})
}
});
};
// 刷新数据
const handleRefresh = async () => {
if (activeTab.value === 'pending') {
await fetchPendingUsers()
await fetchPendingUsers();
} else {
loading.value = true
loading.value = true;
try {
await userStore.fetchUsers()
message.success('刷新成功')
await userStore.fetchUsers();
message.success('刷新成功');
} catch (error) {
message.error(error.message || '刷新失败')
message.error(error.message || '刷新失败');
} finally {
loading.value = false
loading.value = false;
}
}
}
};
// 创建用户
const handleCreate = () => {
dialogMode.value = 'create'
dialogMode.value = 'create';
formData.value = {
alias: '',
role: 'user',
@@ -405,13 +407,13 @@ const handleCreate = () => {
email: '',
password: '',
reset_password: false,
}
dialogVisible.value = true
}
};
dialogVisible.value = true;
};
// 编辑用户
const handleEdit = (user) => {
dialogMode.value = 'edit'
const handleEdit = user => {
dialogMode.value = 'edit';
formData.value = {
id: user.id,
alias: user.alias,
@@ -420,44 +422,44 @@ const handleEdit = (user) => {
email: user.email || '',
password: '',
reset_password: false,
}
dialogVisible.value = true
}
};
dialogVisible.value = true;
};
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
if (!formRef.value) return;
try {
await formRef.value.validate()
submitting.value = true
await formRef.value.validate();
submitting.value = true;
// 检查密码设置冲突
if (dialogMode.value === 'edit' && formData.value.password && formData.value.reset_password) {
message.warning('不能同时设置新密码和重置密码,请选择其一')
submitting.value = false
return
message.warning('不能同时设置新密码和重置密码,请选择其一');
submitting.value = false;
return;
}
if (dialogMode.value === 'create') {
await userStore.createUser(formData.value)
message.success('创建成功')
await userStore.createUser(formData.value);
message.success('创建成功');
} else {
await userStore.updateUser(formData.value.id, formData.value)
message.success('更新成功')
await userStore.updateUser(formData.value.id, formData.value);
message.success('更新成功');
}
dialogVisible.value = false
await handleRefresh()
dialogVisible.value = false;
await handleRefresh();
} catch (error) {
message.error(error.message || '操作失败')
message.error(error.message || '操作失败');
} finally {
submitting.value = false
submitting.value = false;
}
}
};
// 删除用户
const handleDelete = (user) => {
const handleDelete = user => {
Modal.confirm({
title: '警告',
content: `确定要删除用户 "${user.alias}" `,
@@ -466,15 +468,15 @@ const handleDelete = (user) => {
okType: 'danger',
onOk: async () => {
try {
await userStore.deleteUser(user.id)
message.success('删除成功')
await handleRefresh()
await userStore.deleteUser(user.id);
message.success('删除成功');
await handleRefresh();
} catch (error) {
message.error(error.message || '删除失败')
message.error(error.message || '删除失败');
}
},
})
}
});
};
// 批量审批
const handleBatchApprove = () => {
@@ -484,24 +486,24 @@ const handleBatchApprove = () => {
okText: '确认',
cancelText: '取消',
onOk: async () => {
const userIds = selectedUsers.value.map((u) => u.id)
let successCount = 0
let failureCount = 0
const userIds = selectedUsers.value.map(u => u.id);
let successCount = 0;
let failureCount = 0;
for (const userId of userIds) {
try {
await adminAPI.approveUser(userId)
successCount++
} catch (error) {
failureCount++
await adminAPI.approveUser(userId);
successCount++;
} catch {
failureCount++;
}
}
message.success(`批量审批完成成功 ${successCount}失败 ${failureCount}`)
await handleRefresh()
message.success(`批量审批完成成功 ${successCount}失败 ${failureCount}`);
await handleRefresh();
},
})
}
});
};
// 批量删除
const handleBatchDelete = () => {
@@ -512,29 +514,29 @@ const handleBatchDelete = () => {
cancelText: '取消',
okType: 'danger',
onOk: async () => {
const userIds = selectedUsers.value.map((u) => u.id)
let successCount = 0
let failureCount = 0
const userIds = selectedUsers.value.map(u => u.id);
let successCount = 0;
let failureCount = 0;
for (const userId of userIds) {
try {
await userStore.deleteUser(userId)
successCount++
} catch (error) {
failureCount++
await userStore.deleteUser(userId);
successCount++;
} catch {
failureCount++;
}
}
message.success(`批量删除完成成功 ${successCount}失败 ${failureCount}`)
await handleRefresh()
message.success(`批量删除完成成功 ${successCount}失败 ${failureCount}`);
await handleRefresh();
},
})
}
});
};
onMounted(() => {
// 默认加载所有用户
handleRefresh()
})
handleRefresh();
});
</script>
<style scoped>