From e0af18b36385cdcef9e2a4d2aec6c80a784377c3 Mon Sep 17 00:00:00 2001 From: Yuanzq <1259216392@qq.com> Date: Fri, 16 Jan 2026 10:27:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20API=20=E6=8E=A5=E5=8F=A3=E3=80=81=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E9=80=BB=E8=BE=91=E3=80=81=E5=B7=A5=E5=85=B7=E7=B1=BB?= =?UTF-8?q?=E3=80=81API=20=E6=B5=8B=E8=AF=95=E8=84=9A=E6=9C=AC=E5=92=8C?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_DOCUMENTATION.md | 224 ++++++++++++++++++ .../face/controller/UserController.java | 19 ++ .../com/bonuos/face/service/UserService.java | 9 + .../face/service/impl/UserServiceImpl.java | 94 ++++++++ .../java/com/bonuos/face/util/FaceUtils.java | 58 +++++ .../face/controller/UserController.class | Bin 5406 -> 6111 bytes .../com/bonuos/face/service/UserService.class | Bin 975 -> 1110 bytes .../face/service/impl/UserServiceImpl.class | Bin 9045 -> 11558 bytes .../com/bonuos/face/util/FaceUtils$1.class | Bin 0 -> 611 bytes .../com/bonuos/face/util/FaceUtils.class | Bin 0 -> 2082 bytes quick_test_api.py | 103 ++++++++ verify_changes.py | 78 ++++++ 12 files changed, 585 insertions(+) create mode 100644 API_DOCUMENTATION.md create mode 100644 backend-java/src/main/java/com/bonuos/face/util/FaceUtils.java create mode 100644 backend-java/target/classes/com/bonuos/face/util/FaceUtils$1.class create mode 100644 backend-java/target/classes/com/bonuos/face/util/FaceUtils.class create mode 100644 quick_test_api.py create mode 100644 verify_changes.py diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..2c5d226 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,224 @@ +# 人脸识别系统 API 接口文档 + +本文档详细说明了人脸识别系统的后端业务接口与算法服务接口,供开发人员集成使用。 + +## 1. 系统业务接口 (Java Backend) + +后端服务主要提供分组管理和人员信息管理功能。所有接口统一通过 HTTP 协议调用,返回 JSON 格式数据。 + +**基础路径**: `/api` (例如: `http://:/api`) + +### 1.1 分组管理 + +#### 1.1.1 创建/添加分组 +* **接口地址**: `/groups/add` +* **请求方式**: `POST` +* **请求类型**: `application/json` +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | name | String | 是 | 分组名称 | + | description | String | 否 | 分组描述 (如果有) | + + **请求示例**: + ```json + { + "name": "VIP人员", + "description": "重要客户分组" + } + ``` + +* **返回结果**: + ```json + { + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "VIP人员", + "createTime": "2023-12-30T10:00:00" + } + } + ``` + +#### 1.1.2 获取分组列表 +* **接口地址**: `/groups/list` +* **请求方式**: `GET` +* **返回结果**: + ```json + { + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "默认分组" + }, + { + "id": 2, + "name": "员工分组" + } + ] + } + ``` + +--- + +### 1.2 人员管理 + +#### 1.2.1 新增人员 (带照片) +该接口用于注册新用户,同时上传人脸照片用于提取特征。 + +* **接口地址**: `/users/add` +* **请求方式**: `POST` +* **请求类型**: `multipart/form-data` (表单上传) +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | name | String | 是 | 人员姓名 | + | groupId | Long | 否 | 所属分组ID | + | photo | File | 是 | 人脸照片文件 (jpg/png) | + +* **返回结果**: + ```json + { + "code": 200, + "msg": "success", + "data": { + "id": 101, + "name": "张三", + "groupId": 2, + "feature": "..." // (内部可能不直接返回长特征,视具体实现而定) + } + } + ``` + +#### 1.2.2 编辑人员信息 +用于更新人员姓名、分组或重新上传人脸照片。 + +* **接口地址**: `/users/update/{id}` +* **请求方式**: `POST` +* **请求类型**: `multipart/form-data` +* **路径参数**: + * `id`: 人员ID (Long) + +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | name | String | 是 | 人员姓名 | + | groupId | Long | 否 | 所属分组ID (不传则不修改) | + | photo | File | 否 | 新的人脸照片 (不传则不修改) | + +* **返回结果**: + ```json + { + "code": 200, + "msg": "success", + "data": { ... } + } + ``` + +#### 1.2.3 获取人员列表 (查询) +* **接口地址**: `/users/list` +* **请求方式**: `GET` +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | name | String | 否 | 按姓名模糊查询 | + | groupId | Long | 否 | 按分组ID筛选 | + +* **返回结果**: + ```json + { + "code": 200, + "msg": "success", + "data": [ ...User Objects... ] + } + ``` + +#### 1.2.4 人脸搜索 (1:N 检索) +用于上传一张人脸照片,并在指定分组内检索最相似的人员。 + +* **接口地址**: `/users/search` +* **请求方式**: `POST` +* **请求类型**: `multipart/form-data` +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | photo | File | 是 | 待检索的人脸照片 | + | groupId | Long | 是 | 目标分组ID | + +* **返回结果**: + + **成功找到匹配**: + ```json + { + "code": 200, + "msg": "success", + "data": { + "id": 101, + "name": "张三", + "groupId": 2, + ... + } + } + ``` + + **未找到匹配 (相似度低于阈值 0.6)**: + ```json + { + "code": 404, + "msg": "未找到匹配用户" + } + ``` + +--- + +## 2. 算法服务接口 (Python - FaceFeatureExtractor) + +该接口通常由 Java 后端内部调用,但如果开发人员需要独立调试算法或进行集成,可直接调用该微服务。 + +**功能**: 接收图片,进行质量检测(人脸检测、模糊度、亮度、姿态等),合格后提取 1024 维人脸特征向量。 + +**默认端口**: `8000` (具体取决于 docker-compose 配置) + +### 2.1 提取人脸特征 +* **接口地址**: `/api/extract_feature` +* **请求方式**: `POST` +* **请求类型**: `multipart/form-data` +* **请求参数**: + + | 参数名 | 类型 | 必填 | 说明 | + | :--- | :--- | :--- | :--- | + | image | File | 是 | 图片文件 (支持 jpg, png 等常见格式) | + +* **返回结果 (JSON)**: + + **成功响应**: + ```json + { + "success": true, + "message": "Success", + "feature": [0.123, -0.456, ...], // 1024维浮点数组 + "feature_dim": 1024, + "processing_time": 0.045 + } + ``` + + **失败/质量不合格响应**: + ```json + { + "success": false, + "message": "No face detected" // 或 "Face quality check failed...", "Server error..." + } + ``` + +### 2.2 健康检查 +* **接口地址**: `/health` +* **请求方式**: `GET` +* **返回结果**: `{"status": "healthy", "service": "Face Feature Extractor"}` diff --git a/backend-java/src/main/java/com/bonuos/face/controller/UserController.java b/backend-java/src/main/java/com/bonuos/face/controller/UserController.java index 9821e7d..c0f732e 100644 --- a/backend-java/src/main/java/com/bonuos/face/controller/UserController.java +++ b/backend-java/src/main/java/com/bonuos/face/controller/UserController.java @@ -106,4 +106,23 @@ public class UserController { result.put("data", count); return result; } + + @PostMapping("/search") + public Map searchUser( + @RequestParam("photo") MultipartFile photo, + @RequestParam("groupId") Long groupId) { + + User user = userService.searchUser(photo, groupId); + + Map result = new HashMap<>(); + if (user != null) { + result.put("code", 200); + result.put("msg", "success"); + result.put("data", user); + } else { + result.put("code", 404); + result.put("msg", "未找到匹配用户"); + } + return result; + } } diff --git a/backend-java/src/main/java/com/bonuos/face/service/UserService.java b/backend-java/src/main/java/com/bonuos/face/service/UserService.java index 0a2ae99..641ad90 100644 --- a/backend-java/src/main/java/com/bonuos/face/service/UserService.java +++ b/backend-java/src/main/java/com/bonuos/face/service/UserService.java @@ -58,5 +58,14 @@ public interface UserService { * * @param id 用户ID */ + /** + * 人脸搜索 + * + * @param photo 人脸照片 + * @param groupId 分组ID + * @return 匹配的用户(如果匹配度过低返回null) + */ + User searchUser(MultipartFile photo, Long groupId); + void deleteUser(Long id); } diff --git a/backend-java/src/main/java/com/bonuos/face/service/impl/UserServiceImpl.java b/backend-java/src/main/java/com/bonuos/face/service/impl/UserServiceImpl.java index 8e8d7c5..b22ea5f 100644 --- a/backend-java/src/main/java/com/bonuos/face/service/impl/UserServiceImpl.java +++ b/backend-java/src/main/java/com/bonuos/face/service/impl/UserServiceImpl.java @@ -41,6 +41,8 @@ public class UserServiceImpl implements UserService { if (userGroupMapper.selectById(groupId) == null) { throw new RuntimeException("指定的用户分组不存在"); } + // 检查重名 + checkDuplicateName(name, groupId, null); } // 确保上传目录存在 @@ -142,6 +144,12 @@ public class UserServiceImpl implements UserService { throw new RuntimeException("指定的用户分组不存在"); } } + + // 如果修改了名字或分组,检查重名 + if (!user.getName().equals(name) || (groupId != null && !groupId.equals(user.getGroupId()))) { + checkDuplicateName(name, groupId != null ? groupId : user.getGroupId(), id); + } + user.setName(name); user.setGroupId(groupId); user.setUpdateTime(LocalDateTime.now()); @@ -220,4 +228,90 @@ public class UserServiceImpl implements UserService { } } } + + @Override + public User searchUser(MultipartFile photo, Long groupId) { + if (groupId == null) { + throw new RuntimeException("必须指定分组ID"); + } + + // 1. 保存上传的照片 (临时) + String fileName = IdUtil.fastSimpleUUID() + "_" + photo.getOriginalFilename(); + File uploadDir = new File(uploadPath); + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + File dest = new File(uploadDir, fileName); + + try { + photo.transferTo(dest); + } catch (IOException e) { + throw new RuntimeException("照片处理失败", e); + } + + try { + // 2. 提取特征 + List targetFeature = faceFeatureExtractorClient.extractFeature(dest); + if (targetFeature == null || targetFeature.isEmpty()) { + throw new RuntimeException("未能从上传图片中提取到人脸特征"); + } + + // 3. 获取分组下所有用户 + List users = getUsers(null, groupId); + if (users == null || users.isEmpty()) { + return null; + } + + // 4. 比对特征 + User bestMatch = null; + float maxSimilarity = 0f; + float threshold = 0.6f; // 相似度阈值 + + for (User user : users) { + if (user.getFeatureData() == null) + continue; + + List dbFeature = com.bonuos.face.util.FaceUtils.parseFeatureData(user.getFeatureData()); + if (dbFeature != null) { + float similarity = com.bonuos.face.util.FaceUtils.cosineSimilarity(targetFeature, dbFeature); + if (similarity > maxSimilarity) { + maxSimilarity = similarity; + bestMatch = user; + } + } + } + + // 5. 返回结果 + if (maxSimilarity >= threshold) { + return bestMatch; + } + return null; // 未找到匹配 + + } catch (Exception e) { + throw new RuntimeException("人脸搜索失败: " + e.getMessage(), e); + } finally { + // 清理临时文件 + if (dest.exists()) { + dest.delete(); + } + } + } + + /** + * 检查分组下是否存在同名用户 + */ + private void checkDuplicateName(String name, Long groupId, Long excludeId) { + com.baomidou.mybatisplus.core.conditions.query.QueryWrapper wrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(); + wrapper.eq("group_id", groupId) + .eq("name", name) + .eq("status", 1); // 只检查有效用户 + + if (excludeId != null) { + wrapper.ne("id", excludeId); + } + + if (userMapper.selectCount(wrapper) > 0) { + throw new RuntimeException("该分组下已存在名为 [" + name + "] 的用户"); + } + } } diff --git a/backend-java/src/main/java/com/bonuos/face/util/FaceUtils.java b/backend-java/src/main/java/com/bonuos/face/util/FaceUtils.java new file mode 100644 index 0000000..9de2a47 --- /dev/null +++ b/backend-java/src/main/java/com/bonuos/face/util/FaceUtils.java @@ -0,0 +1,58 @@ +package com.bonuos.face.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; + +public class FaceUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 计算两个特征向量的余弦相似度 + * + * @param feature1 特征向量1 + * @param feature2 特征向量2 + * @return 相似度 [-1, 1] + */ + public static float cosineSimilarity(List feature1, List feature2) { + if (feature1 == null || feature2 == null || feature1.size() != feature2.size() || feature1.isEmpty()) { + return -1f; + } + + float dotProduct = 0.0f; + float normA = 0.0f; + float normB = 0.0f; + + for (int i = 0; i < feature1.size(); i++) { + dotProduct += feature1.get(i) * feature2.get(i); + normA += Math.pow(feature1.get(i), 2); + normB += Math.pow(feature2.get(i), 2); + } + + if (normA == 0 || normB == 0) { + return 0.0f; + } + + return (float) (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))); + } + + /** + * 解析特征数据字符串 + * + * @param featureDataJson JSON字符串 [0.1, 0.2, ...] + * @return 特征列表 + */ + public static List parseFeatureData(String featureDataJson) { + if (featureDataJson == null || featureDataJson.isEmpty()) { + return null; + } + try { + return objectMapper.readValue(featureDataJson, new TypeReference>() { + }); + } catch (Exception e) { + return null; + } + } +} diff --git a/backend-java/target/classes/com/bonuos/face/controller/UserController.class b/backend-java/target/classes/com/bonuos/face/controller/UserController.class index 1684a1369fa36f418e9401da111c4a825989db5a..78632db3cd06b2e5e8409436be32436d36ece2e4 100644 GIT binary patch delta 476 zcmYk2Pfrt36vcltZ8NkT3pA8E*rY@kr2|a~YC=p5)hezu1cSt|poJ7zcT%WgU}aJh ziptbK7~@vMsz#Gqo3J3!g*!h1D{A}>YP>TIfyI08p7YK-_q_YgJWL0E{QJ5OT;wWyUHL6{d&mpQ>Spd}r+`?Yvq(4dZq?nC#cm9et1sJlEM`M0X9kBynmqGxr$yp^gX9%&>N_br-<{ZePu%c^ z&0eMU1rd#Tm=!!8wvX$&czC5fm;_|E&?ymNRU)jBq|9X=kl`U&*15|g=GmZIl$SdF z>7iGUB$1FAiRn}^Cgf{6OZT|Ob%`3`wiw)yRyReUi>$*;3X^jg^)FeCyvD_I+!DsG biXYo0Ww*@ delta 64 zcmcbwKTnJ6)W2Q(7#J9A876Mz+RZcBgzpd|$L8;RrR<7K3_U;zCWc-Hb|5VTWc2~* Peg;Mc@n{CdVulF-bnOo^ diff --git a/backend-java/target/classes/com/bonuos/face/service/UserService.class b/backend-java/target/classes/com/bonuos/face/service/UserService.class index 5ca00d0a16e18250a3963cdb7bf5f07f05ef3ce1..c52a630c46a3f6b940a256a4f61fc1486aa3f244 100644 GIT binary patch delta 91 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 bA<5}ut+rWf?Qy+q;1&NgVtDKT6ch zZX6tt$<%vQ-Sg^I&By24JAeZ$mr)Q{jcrf1tx2ty9Tk&IgHB|F*%cGNzFS5~U^~0j zQ9h7+>Lv0{#rNJC8Cyp(JU@_Zak!)oIWjQ`4DV~B!=XTNWAj>|bYyR+f+9w%7{ai? zWTXvUq`fw|mTD*bMJ=+iO0JdDSw1M1!maiKYtdgO>jIUgO|F8A+g_M=Rqzn4h+Fvt^Sy0Vd273zol@JuHrPOR6adrslj-jp#bu=2a} zD(0~uF!i9Erv~K)k5yp9>l^*Hd7EZ%EL-3F;e?x3&x|2=lqm0sH#KrWKAc(~unJy2 z|7S2FTRLNznPki>Tdx>-;X(lwj@dD=hcS-T{sqQy&W13-v4BZTcF MK#(bx_$^@O3%DVj`~Uy| literal 0 HcmV?d00001 diff --git a/backend-java/target/classes/com/bonuos/face/util/FaceUtils.class b/backend-java/target/classes/com/bonuos/face/util/FaceUtils.class new file mode 100644 index 0000000000000000000000000000000000000000..7aab8ebf871b9b38692205a19a3b0cc2b4b7390e GIT binary patch literal 2082 zcmb7FOLG)e7(I6~kEEybFytXgAaBS_gfWOoG%*56MuU?O$U{KX>df3E9cH@6?j8lp zvRqg$sG5?6EaFBf7w%F~sQ^-?tXTMyv`Rd;XUMC9rBl z8~8ASfWXnTol7j)R>5`?%SKuz3Z9uwB-vYF$BiH;P`74$Y$URVwUW57v?kM@fNuMp zX(OMPj=+hOao^)>HvWYb#eciRKM8@!pCDY23_m#{?1S&_={-^oEHv1AmjI1)jdK+RqZI*y{92XoC&8IUwjDbr3J zUFa5wnC@gQufTKVYVh|j0JVHf$8q#=az!%BjeS$GUARigABiANZg(`{!_^SOOFB;A zq=2@p4zOrs3#Gux@EIyj{WP4y=?GqV2G&3BwBfDlcol;To3}p^2=z@q#Ou#VU8 zx;n&14)Fy0I8XH~OP|I8CSyO*JFBM%Bx8HCj-4qe$qJ?*WZ6#cZ25hTb0(iDfu_B> zxS20$gPGmu|DYTQRL^-7Jk?#6S>3esR_)BJ;TSo|D?wq^PLVruh1iCSMEdq1+_nM7{Ci`!!%OTulHw^ z6BexU#suGLH59*(L;JT>`djpp0UFRq!zwhPnOI)*ogG%u#pev6Fx`tkL3{rbw8kHz zg_YEAzlY9Yhrl|*^*tNW$!Ib<9IO}&{fKKl@y5_Y^ak)4u`TrX?-{KvP4*V|8WbCl zcz}WQ?PPPXxYv4nX*l?0NHm5@5bi$xt~YvT9a?iRdgoh&q8mNM-r@rcaX;JZ_U#Sg z4*E0=qMH2%bnu+rJYO#kvlg`=jyAlEBba49TSGg(;`kdDvAeYT0mpof#!BbM2uA6H zOz`_QO5S?$HOBBJ6LB0xoMEqm{%+wcdqEbf6`aF3GdjUNO@0pV(Z}Srk4gGSa@2-! zN8l-Rgd=F>KM>%j6My0IpJ*0%ixyP@Zd9LDm7=#XwVk5#zCb=>I3>5aJ3A&5*1N#F+x8IU`Wsibon0J0IuPEzBQW8@U4Cy@CmB%KSN~TasU7T literal 0 HcmV?d00001 diff --git a/quick_test_api.py b/quick_test_api.py new file mode 100644 index 0000000..a1b3089 --- /dev/null +++ b/quick_test_api.py @@ -0,0 +1,103 @@ + +import requests +import json +import cv2 +import numpy as np +import os + +# 配置服务地址 +JAVA_BACKEND_URL = "http://localhost:18080" +PYTHON_ALGO_URL = "http://localhost:18000" + +def create_test_image(filename="test_face.jpg"): + """创建一张简单的测试图片(如果不存在)""" + if not os.path.exists(filename): + print(f"Creating test image: {filename}...") + # 创建一个 640x480 的黑色图像 + img = np.zeros((480, 640, 3), np.uint8) + # 画一个简单的圆代表"脸" (虽然检测不到,但可以测试文件上传流程) + cv2.circle(img, (320, 240), 100, (255, 255, 255), -1) + cv2.imwrite(filename, img) + return filename + +def test_python_health(): + """测试 Python 算法服务健康检查""" + url = f"{PYTHON_ALGO_URL}/health" + print(f"\n[Testing] Python Algorithm Service Health ({url})...") + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + print(f"✅ Success: {response.json()}") + else: + print(f"❌ Failed: Status {response.status_code}, Response: {response.text}") + except requests.exceptions.ConnectionError: + print("❌ Failed: Connection refused. Is the service running on port 18000?") + except Exception as e: + print(f"❌ Error: {e}") + +def test_extract_feature(image_path): + """测试人脸特征提取接口""" + url = f"{PYTHON_ALGO_URL}/api/extract_feature" + print(f"\n[Testing] Face Feature Extraction ({url})...") + + if not os.path.exists(image_path): + print("❌ Error: Test image not found.") + return + + try: + with open(image_path, 'rb') as f: + files = {'image': f} + response = requests.post(url, files=files, timeout=10) + + if response.status_code == 200: + result = response.json() + if result.get('success'): + dim = result.get('feature_dim') + print(f"✅ Success: Feature extracted. Dimension: {dim}") + else: + # 预期内失败,因为我们的假脸可能过不了检测,但这证明接口通了 + print(f"⚠️ Service Reachable (Logic Result): {result.get('message')}") + else: + print(f"❌ Failed: Status {response.status_code}, Response: {response.text}") + except requests.exceptions.ConnectionError: + print("❌ Failed: Connection refused.") + except Exception as e: + print(f"❌ Error: {e}") + +def test_java_group_list(): + """测试 Java 后端分组列表接口""" + url = f"{JAVA_BACKEND_URL}/api/groups/list" + print(f"\n[Testing] Java Backend Group List ({url})...") + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('code') == 200: + print(f"✅ Success: Retrieved {len(data.get('data', []))} groups.") + print(f" Response: {json.dumps(data, ensure_ascii=False)}") + else: + print(f"❌ Functional Error: {data}") + else: + print(f"❌ Failed: Status {response.status_code}, Response: {response.text}") + except requests.exceptions.ConnectionError: + print("❌ Failed: Connection refused. Is the service running on port 18080?") + except Exception as e: + print(f"❌ Error: {e}") + +if __name__ == "__main__": + print("=== Face Recognition System Quick Test ===") + + # 1. 准备测试图片 + test_img = create_test_image() + + # 2. 测试算法服务 + test_python_health() + test_extract_feature(test_img) + + # 3. 测试Java后端 + test_java_group_list() + + print("\n=== Test Finished ===") + # 清理生成的测试图 (可选,这里保留以便用户查看) + # if os.path.exists(test_img): + # os.remove(test_img) diff --git a/verify_changes.py b/verify_changes.py new file mode 100644 index 0000000..2ad92d4 --- /dev/null +++ b/verify_changes.py @@ -0,0 +1,78 @@ + +import requests +import json +import os + +# 配置 +BASE_URL = "http://localhost:18080/api" +GROUP_NAME = "TestGroup_Verify" +USER_NAME = "DuplicateTestUser" +TEST_IMG = "verify_face.jpg" + +def create_test_image(): + if not os.path.exists(TEST_IMG): + # 创建一个简单的dummy文件 + with open(TEST_IMG, 'wb') as f: + f.write(b'\xFF\xD8\xFF\xE0' + b'\x00' * 1000) # Fake JPG header + return TEST_IMG + +def run_verification(): + print("=== 开始验证 ===") + + # 1. 创建测试分组 + print("\n1. 创建分组...") + resp = requests.post(f"{BASE_URL}/groups/add", json={"name": GROUP_NAME}) + if resp.status_code == 200 and resp.json()['code'] == 200: + group_id = resp.json()['data']['id'] + print(f"✅ 分组创建成功 ID: {group_id}") + else: + print(f"❌ 分组创建失败: {resp.text}") + return + + # 准备图片 + create_test_image() + files = {'photo': open(TEST_IMG, 'rb')} + + # 2. 添加用户 (第一次) + print("\n2. 添加用户 (预期成功)...") + data = {"name": USER_NAME, "groupId": group_id} + # Re-open file for each request + resp = requests.post(f"{BASE_URL}/users/add", data=data, files={'photo': open(TEST_IMG, 'rb')}) + + if resp.status_code == 200 and resp.json()['code'] == 200: + print(f"✅ 用户创建成功") + else: + # 如果是因为测试图片无法提取特征而失败,也是预期的(说明代码跑到了特征提取那一步) + # 但我们主要验证重名逻辑,所以这里假设如果失败是因为重名则是有问题,如果是因为特征提取则是环境问题 + print(f"⚠️ 用户创建返回: {resp.json().get('msg')}") + + # 3. 添加同名用户 (预期失败) + print("\n3. 再次添加同名用户 (预期被拦截)...") + resp = requests.post(f"{BASE_URL}/users/add", data=data, files={'photo': open(TEST_IMG, 'rb')}) + + res_json = resp.json() + if res_json.get('code') != 200 and "已存在" in str(res_json.get('msg')): + print(f"✅ 成功拦截重名用户: {res_json.get('msg')}") + else: + print(f"❌ 拦截失败或错误信息不匹配: {resp.text}") + + # 4. 测试搜索接口 + print("\n4. 测试人脸搜索接口...") + try: + resp = requests.post(f"{BASE_URL}/users/search", data={"groupId": group_id}, files={'photo': open(TEST_IMG, 'rb')}) + if resp.status_code == 200: + print(f"✅ 接口调用成功: {resp.json()}") + elif resp.status_code == 404: + print(f"✅ 接口调用成功 (未找到匹配 - 符合预期): {resp.json()}") + else: + print(f"❌ 接口调用异常: {resp.status_code} - {resp.text}") + except Exception as e: + print(f"❌ 请求异常: {e}") + + print("\n=== 验证结束 ===") + +if __name__ == "__main__": + try: + run_verification() + except Exception as e: + print(f"Fatal Error: {e}")