加入分组查询、姓名查询,修复了页面闪烁问题,人脸质量等统一提示问题
This commit is contained in:
parent
499d5426c8
commit
0cf047ed03
|
|
@ -471,6 +471,15 @@ class FaceFeatureExtractor:
|
|||
processing_time) / self.stats['total_extractions']
|
||||
)
|
||||
|
||||
# Check if we detected faces but filtered them all out due to quality
|
||||
if not face_infos and boxes:
|
||||
return FeatureExtractionResult(
|
||||
success=False,
|
||||
faces=[],
|
||||
processing_time=processing_time,
|
||||
error_message="Face quality check failed (brightness/clarity/pose)"
|
||||
)
|
||||
|
||||
return FeatureExtractionResult(
|
||||
success=len(face_infos) > 0,
|
||||
faces=face_infos,
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ public class UserController {
|
|||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> listUsers() {
|
||||
List<User> users = userService.getAllUsers();
|
||||
public Map<String, Object> listUsers(
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "groupId", required = false) Long groupId) {
|
||||
List<User> users = userService.getUsers(name, groupId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
|
|
@ -94,4 +96,14 @@ public class UserController {
|
|||
result.put("msg", "批量删除成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/count")
|
||||
public Map<String, Object> countUsers() {
|
||||
long count = userService.countUsers();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("msg", "success");
|
||||
result.put("data", count);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.bonuos.face.handler;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public Map<String, Object> handleRuntimeException(RuntimeException e) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
|
||||
// 提取核心错误信息,去除 "java.lang.RuntimeException: " 前缀
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.contains(": ")) {
|
||||
// 如果是包装过的异常,尝试提取更有意义的部分,但这里简单返回 message 即可,
|
||||
// 因为我们在 Service 层已经精心构造了 message
|
||||
}
|
||||
|
||||
result.put("msg", msg != null ? msg : "系统内部错误");
|
||||
return result;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Map<String, Object> handleException(Exception e) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("msg", "系统未知错误: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,15 @@ public interface UserService {
|
|||
User addUser(String name, Long groupId, MultipartFile photo);
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
* 查询用户列表(支持过滤)
|
||||
*
|
||||
* @param name 姓名(模糊查询)
|
||||
* @param groupId 分组ID
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<User> getAllUsers();
|
||||
List<User> getUsers(String name, Long groupId);
|
||||
|
||||
long countUsers();
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.bonuos.face.service.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.bonuos.face.entity.User;
|
||||
import com.bonuos.face.mapper.UserMapper;
|
||||
|
|
@ -64,6 +63,13 @@ public class UserServiceImpl implements UserService {
|
|||
List<Float> features = faceFeatureExtractorClient.extractFeature(dest);
|
||||
featureJson = convertFeaturesToJson(features);
|
||||
} catch (IOException e) {
|
||||
// 处理特征提取失败的异常
|
||||
if (e.getMessage().contains("No face detected") ||
|
||||
e.getMessage().contains("未检测到人脸") ||
|
||||
e.getMessage().contains("Face quality check failed") ||
|
||||
e.getMessage().toLowerCase().contains("feature extraction failed")) {
|
||||
throw new RuntimeException("请上传正确且清晰的人脸照片", e);
|
||||
}
|
||||
throw new RuntimeException("特征提取失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +101,28 @@ public class UserServiceImpl implements UserService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<User> getAllUsers() {
|
||||
public List<User> getUsers(String name, Long groupId) {
|
||||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||||
// 只返回活跃用户(status=1)
|
||||
return userMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>()
|
||||
.eq("status", 1));
|
||||
queryWrapper.eq("status", 1);
|
||||
|
||||
if (name != null && !name.isEmpty()) {
|
||||
queryWrapper.like("name", name);
|
||||
}
|
||||
|
||||
if (groupId != null) {
|
||||
queryWrapper.eq("group_id", groupId);
|
||||
}
|
||||
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
|
||||
return userMapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsers() {
|
||||
return userMapper
|
||||
.selectCount(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>().eq("status", 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -142,6 +165,13 @@ public class UserServiceImpl implements UserService {
|
|||
List<Float> features = faceFeatureExtractorClient.extractFeature(dest);
|
||||
user.setFeatureData(convertFeaturesToJson(features));
|
||||
} catch (IOException e) {
|
||||
// 处理特征提取失败的异常
|
||||
if (e.getMessage().contains("No face detected") ||
|
||||
e.getMessage().contains("未检测到人脸") ||
|
||||
e.getMessage().contains("Face quality check failed") ||
|
||||
e.getMessage().toLowerCase().contains("feature extraction failed")) {
|
||||
throw new RuntimeException("请上传正确且清晰的人脸照片", e);
|
||||
}
|
||||
throw new RuntimeException("照片保存或特征提取失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ public class FaceFeatureExtractorClient {
|
|||
} else {
|
||||
throw new IOException("API call failed with status: " + response.getStatusCode());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error calling Face Feature Extractor API", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,11 +126,17 @@
|
|||
object-fit: cover;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
|
|
@ -187,7 +193,7 @@
|
|||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ users.length }}</h2>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ totalUserCount }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
|
|
@ -203,7 +209,18 @@
|
|||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-select v-model="searchForm.groupId" placeholder="分组查询" clearable
|
||||
style="width: 150px" @change="loadUsers">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName"
|
||||
:value="g.id"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="searchForm.name" placeholder="姓名查询" style="width: 150px"
|
||||
clearable @clear="loadUsers" @keyup.enter="loadUsers">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="loadUsers" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
|
|
@ -357,6 +374,9 @@
|
|||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
|
||||
// Stats
|
||||
const totalUserCount = ref(0);
|
||||
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
|
|
@ -382,10 +402,27 @@
|
|||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const searchForm = reactive({ name: '', groupId: null });
|
||||
|
||||
const loadUserCount = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/count`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
totalUserCount.value = res.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load user count", e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/list`).then(r => r.json());
|
||||
const params = new URLSearchParams();
|
||||
if (searchForm.name) params.append('name', searchForm.name);
|
||||
if (searchForm.groupId) params.append('groupId', searchForm.groupId);
|
||||
|
||||
const res = await fetch(`${API_USER}/list?${params.toString()}`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
|
|
@ -442,6 +479,7 @@
|
|||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
|
|
@ -459,6 +497,7 @@
|
|||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
|
|
@ -491,6 +530,7 @@
|
|||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
|
|
@ -544,6 +584,7 @@
|
|||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
await loadUserCount();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -558,11 +599,13 @@
|
|||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
searchForm,
|
||||
totalUserCount,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh
|
||||
Refresh: ElementPlusIconsVue.Refresh,
|
||||
Search: ElementPlusIconsVue.Search
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -575,6 +618,6 @@
|
|||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -126,11 +126,17 @@
|
|||
object-fit: cover;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<el-container style="height: 100vh;">
|
||||
<el-aside width="220px">
|
||||
<div
|
||||
|
|
@ -187,7 +193,7 @@
|
|||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span>👤 总用户数</span></template>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ users.length }}</h2>
|
||||
<h2 style="margin: 0; color: #409EFF;">{{ totalUserCount }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
|
|
@ -203,7 +209,18 @@
|
|||
<div v-show="activeMenu === 'users'">
|
||||
<div class="page-header">
|
||||
<span class="page-title">用户列表</span>
|
||||
<div>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<el-select v-model="searchForm.groupId" placeholder="分组查询" clearable
|
||||
style="width: 150px" @change="loadUsers">
|
||||
<el-option v-for="g in groups" :key="g.id" :label="g.groupName"
|
||||
:value="g.id"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="searchForm.name" placeholder="姓名查询" style="width: 150px"
|
||||
clearable @clear="loadUsers" @keyup.enter="loadUsers">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="loadUsers" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="loadUsers" :icon="Refresh" circle></el-button>
|
||||
<el-button type="danger" :disabled="selectedUsers.length === 0"
|
||||
@click="handleBatchDelete">批量删除</el-button>
|
||||
|
|
@ -357,6 +374,9 @@
|
|||
const selectedFile = ref(null);
|
||||
const selectedUsers = ref([]);
|
||||
|
||||
// Stats
|
||||
const totalUserCount = ref(0);
|
||||
|
||||
// Dialog States
|
||||
const userDialog = reactive({ visible: false, isEdit: false });
|
||||
const groupDialog = reactive({ visible: false });
|
||||
|
|
@ -382,10 +402,27 @@
|
|||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const searchForm = reactive({ name: '', groupId: null });
|
||||
|
||||
const loadUserCount = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/count`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
totalUserCount.value = res.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load user count", e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_USER}/list`).then(r => r.json());
|
||||
const params = new URLSearchParams();
|
||||
if (searchForm.name) params.append('name', searchForm.name);
|
||||
if (searchForm.groupId) params.append('groupId', searchForm.groupId);
|
||||
|
||||
const res = await fetch(`${API_USER}/list?${params.toString()}`).then(r => r.json());
|
||||
if (res.code === 200) {
|
||||
users.value = res.data;
|
||||
}
|
||||
|
|
@ -442,6 +479,7 @@
|
|||
ElementPlus.ElMessage.success(userDialog.isEdit ? '更新成功' : '注册成功');
|
||||
userDialog.visible = false;
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
loadData(); // Refresh all
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg || '操作失败');
|
||||
|
|
@ -459,6 +497,7 @@
|
|||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
|
|
@ -491,6 +530,7 @@
|
|||
if (res.code === 200) {
|
||||
ElementPlus.ElMessage.success('批量删除成功');
|
||||
loadUsers();
|
||||
loadUserCount();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(res.msg);
|
||||
}
|
||||
|
|
@ -544,6 +584,7 @@
|
|||
const loadData = async () => {
|
||||
await loadGroups();
|
||||
await loadUsers();
|
||||
await loadUserCount();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -558,11 +599,13 @@
|
|||
handleSelectionChange, handleBatchDelete, selectedUsers,
|
||||
groupDialog, groupForm, openGroupDialog, submitGroup, handleDeleteGroup,
|
||||
loadUsers, loadGroups,
|
||||
loadUsers, loadGroups,
|
||||
handleFileChange,
|
||||
handleCommand,
|
||||
searchForm,
|
||||
totalUserCount,
|
||||
Plus: ElementPlusIconsVue.Plus,
|
||||
Refresh: ElementPlusIconsVue.Refresh
|
||||
Refresh: ElementPlusIconsVue.Refresh,
|
||||
Search: ElementPlusIconsVue.Search
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -575,6 +618,6 @@
|
|||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue