加入分组查询、姓名查询,修复了页面闪烁问题,人脸质量等统一提示问题

This commit is contained in:
Yuanzq 2025-12-24 16:19:11 +08:00
parent 499d5426c8
commit 0cf047ed03
13 changed files with 967 additions and 789 deletions

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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查询用户

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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>