Files
FrameTour-BE/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java
Jerry Yan b92568b842 feat(face): 实现账号级人脸识别调度器
- 新增账号级别调度器管理器,支持多账号QPS隔离控制
- 为阿里云和百度云适配器添加配置getter方法
- 移除原有阻塞式限流逻辑,交由外层调度器统一管控
- 创建QPS调度器实现精确的任务频率控制
- 新增监控接口用于查询各账号调度器运行状态
- 重构人脸识别Kafka消费服务,集成账号调度机制
- 优化线程池资源配置,提升多账号并发处理效率
- 增强错误处理与状态更新的安全性
- 删除旧版全局线程池配置类
- 完善任务提交与状态流转的日志记录
2025-11-29 23:50:24 +08:00

489 lines
21 KiB
Java

package com.ycwl.basic.facebody.adapter;
import cn.hutool.core.codec.Base64;
import com.baidu.aip.face.AipFace;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.facebody.entity.BceFaceBodyConfig;
import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.utils.ratelimiter.FixedRateLimiter;
import com.ycwl.basic.utils.ratelimiter.IRateLimiter;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class BceFaceBodyAdapter implements IFaceBodyAdapter {
protected static final Map<String, AipFace> clients = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> addEntityLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> addFaceLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> addDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> listDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> listFaceLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> searchFaceLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteFaceLimiters = new ConcurrentHashMap<>();
@Getter // 添加getter,支持获取appId和addQps
private BceFaceBodyConfig config;
public boolean setConfig(BceFaceBodyConfig config) {
this.config = config;
return true;
}
@Override
public boolean loadConfig(Map<String, String> _config) {
BceFaceBodyConfig config = new BceFaceBodyConfig();
config.setAppId(_config.get("appId"));
config.setApiKey(_config.get("apiKey"));
config.setSecretKey(_config.get("secretKey"));
config.setAddQps(Float.parseFloat(_config.get("addQps")));
config.setSearchQps(Float.parseFloat(_config.get("searchQps")));
this.config = config;
return true;
}
@Override
public boolean addFaceDb(String dbName) {
IRateLimiter addDbLimiter = getLimiter(LOCK_TYPE.ADD_DB);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
try {
addDbLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.groupAdd(dbName, options);
if (response.getInt("error_code") == 0) {
return true;
} else {
log.warn("创建人脸库失败!{}", response);
return false;
}
} catch (Exception e) {
log.error("创建人脸库失败!", e);
return false;
}
}
@Override
public boolean deleteFaceDb(String dbName) {
IRateLimiter deleteDbLimiter = getLimiter(LOCK_TYPE.DELETE_DB);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
try {
deleteDbLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.groupDelete(dbName, options);
if (response.getInt("error_code") == 0) {
return true;
} else {
log.warn("删除人脸库失败!{}", response);
return false;
}
} catch (Exception e) {
log.error("删除人脸库失败!", e);
return false;
}
}
@Override
public List<String> listFaceDb() {
IRateLimiter listDbLimiter = getLimiter(LOCK_TYPE.LIST_DB);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
options.put("start", "0");
options.put("length", "1000");
try {
listDbLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.getGroupList(options);
if (response.getInt("error_code") == 0) {
JSONObject resultObj = response.getJSONObject("result");
if (resultObj != null) {
JSONArray data = resultObj.getJSONArray("group_id_list");
List<String> result = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
result.add(data.getString(i));
}
return result;
} else {
return Collections.emptyList();
}
} else {
log.warn("获取人脸库列表失败!{}", response);
return Collections.emptyList();
}
} catch (Exception e) {
log.error("获取人脸库列表失败!", e);
return Collections.emptyList();
}
}
@Override
public AddFaceResp addFace(String dbName, String entityId, String faceUrl, String extData) {
IRateLimiter addEntityLimiter = getLimiter(LOCK_TYPE.ADD_FACE);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
options.put("user_info", extData);
// options.put("quality_control", "LOW");
options.put("action_type", "REPLACE");
// QPS控制已由外层调度器管理,这里不再需要限流
// 移除阻塞等待: addEntityLimiter.acquire()
JSONObject response = client.addUser(faceUrl, "URL", dbName, entityId, options);
int errorCode = response.getInt("error_code");
if (errorCode == 0) {
AddFaceResp resp = new AddFaceResp();
resp.setScore(100f);
return resp;
} else if (errorCode == 222204) {
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
log.warn("无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
String base64Image = downloadImageAsBase64(faceUrl);
if (base64Image != null) {
// 重试时也不需要限流,由外层调度器控制
JSONObject retryResponse = client.addUser(base64Image, "BASE64", dbName, entityId, options);
if (retryResponse.getInt("error_code") == 0) {
log.info("使用base64重试添加人脸成功,entityId: {}", entityId);
AddFaceResp resp = new AddFaceResp();
resp.setScore(100f);
return resp;
} else {
log.warn("使用base64重试添加人脸仍失败!{}", retryResponse);
return null;
}
} else {
log.error("下载图片转base64失败,无法重试,URL: {}", faceUrl);
return null;
}
} else {
log.warn("创建人脸失败!{}", response);
return null;
}
} catch (Exception e) {
log.error("创建人脸失败!", e);
return null;
}
}
@Override
public boolean deleteFace(String dbName, String entityId) {
IRateLimiter deleteFaceLimiter = getLimiter(LOCK_TYPE.DELETE_FACE);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
List<String> tokenList = listUserFace(dbName, entityId);
if (tokenList == null) {
return false;
}
AtomicInteger count = new AtomicInteger(0);
tokenList.forEach(faceToken -> {
try {
try {
deleteFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.faceDelete(entityId, dbName, faceToken, options);
if (response.getInt("error_code") != 0) {
log.warn("删除人脸失败!{}", response);
} else {
count.incrementAndGet();
}
} catch (Exception e) {
log.error("删除人脸失败!", e);
}
});
return Integer.valueOf(count.get()).equals(tokenList.size());
} catch (Exception e) {
log.error("删除人脸失败!", e);
return false;
}
}
@Override
public List<String> listFace(String dbName, String prefix, Integer offset, Integer size) {
IRateLimiter listFaceLimiter = getLimiter(LOCK_TYPE.LIST_FACE);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
options.put("start", offset == null ? "0" : offset.toString());
options.put("length", size == null ? "1000" : size.toString());
try {
listFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.getGroupUsers(dbName, options);
if (response.getInt("error_code") == 0) {
JSONObject resultObj = response.getJSONObject("result");
if (resultObj != null) {
JSONArray data = resultObj.getJSONArray("user_id_list");
List<String> result = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
result.add(data.getString(i));
}
return result;
} else {
return Collections.emptyList();
}
} else {
log.warn("获取人脸列表失败!{}", response);
return Collections.emptyList();
}
} catch (Exception e) {
log.error("获取人脸列表失败!", e);
return Collections.emptyList();
}
}
public List<String> listUserFace(String dbName, String entityId) {
IRateLimiter listFaceLimiter = getLimiter(LOCK_TYPE.LIST_FACE);
try {
AipFace client = getClient();
HashMap<String, String> options = new HashMap<>();
try {
listFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.faceGetlist(entityId, dbName, options);
if (response.getInt("error_code") == 0) {
JSONObject resultObj = response.getJSONObject("result");
if (resultObj != null) {
try {
JSONArray faceList = resultObj.getJSONArray("face_list");
List<String> result = new ArrayList<>();
for (int i = 0; i < faceList.length(); i++) {
JSONObject jsonObject = faceList.getJSONObject(i);
result.add(jsonObject.getString("face_token"));
}
return result;
} catch (Exception e) {
return Collections.emptyList();
}
} else {
return Collections.emptyList();
}
} else if (response.getInt("error_code") == 223103) {
// 用户不存在
return Collections.emptyList();
} else {
log.warn("获取人脸列表失败!{}", response);
return null;
}
} catch (Exception e) {
log.error("获取人脸列表失败!", e);
return null;
}
}
@Override
public SearchFaceResp searchFace(String dbName, String faceUrl) {
IRateLimiter searchFaceLimiter = getLimiter(LOCK_TYPE.SEARCH_FACE);
SearchFaceResp resp = new SearchFaceResp();
try {
AipFace client = getClient();
HashMap<String, Object> options = new HashMap<>();
options.put("max_user_num", "50");
try {
searchFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject response = client.search(faceUrl, "URL", dbName, options);
int errorCode = response.getInt("error_code");
if (errorCode == 0) {
resp.setOriginalFaceScore(100f);
JSONObject resultObj = response.getJSONObject("result");
if (resultObj == null) {
resp.setFirstMatchRate(0f);
return resp;
}
JSONArray userList = resultObj.getJSONArray("user_list");
List<SearchFaceResultItem> result = new ArrayList<>();
for (int i = 0; i < userList.length(); i++) {
JSONObject user = userList.getJSONObject(i);
SearchFaceResultItem item = new SearchFaceResultItem();
item.setDbName(dbName);
item.setFaceId(user.getString("user_id"));
item.setExtData(user.getString("user_info"));
item.setScore(user.getBigDecimal("score").divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP).floatValue());
result.add(item);
}
resp.setResult(result);
if (!result.isEmpty()) {
resp.setFirstMatchRate(result.getFirst().getScore());
}
return resp;
} else if (errorCode == 222204) {
// error_code: 222204 表示无法正常访问URL图片,尝试下载并转换为base64后重试
log.warn("搜索人脸时无法正常访问URL图片,错误码: 222204,尝试下载图片转base64后重试,URL: {}", faceUrl);
String base64Image = downloadImageAsBase64(faceUrl);
if (base64Image != null) {
try {
searchFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
JSONObject retryResponse = client.search(base64Image, "BASE64", dbName, options);
if (retryResponse.getInt("error_code") == 0) {
log.info("使用base64重试搜索人脸成功");
resp.setOriginalFaceScore(100f);
JSONObject resultObj = retryResponse.getJSONObject("result");
if (resultObj == null) {
resp.setFirstMatchRate(0f);
return resp;
}
JSONArray userList = resultObj.getJSONArray("user_list");
List<SearchFaceResultItem> result = new ArrayList<>();
for (int i = 0; i < userList.length(); i++) {
JSONObject user = userList.getJSONObject(i);
SearchFaceResultItem item = new SearchFaceResultItem();
item.setDbName(dbName);
item.setFaceId(user.getString("user_id"));
item.setExtData(user.getString("user_info"));
item.setScore(user.getBigDecimal("score").divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP).floatValue());
result.add(item);
}
resp.setResult(result);
if (!result.isEmpty()) {
resp.setFirstMatchRate(result.getFirst().getScore());
}
return resp;
} else {
log.warn("使用base64重试搜索人脸仍失败!{}", retryResponse);
resp.setOriginalFaceScore(0f);
return resp;
}
} else {
log.error("下载图片转base64失败,无法重试,URL: {}", faceUrl);
resp.setOriginalFaceScore(0f);
return resp;
}
} else {
log.warn("搜索人脸失败,错误码: {}, 响应: {}", errorCode, response);
resp.setOriginalFaceScore(0f);
return resp;
}
} catch (Exception e) {
log.error("搜索人脸失败!", e);
return null;
}
}
public AipFace getClient() {
if (clients.containsKey(config.getAppId())) {
return clients.get(config.getAppId());
}
synchronized (clients) {
if (clients.containsKey(config.getAppId())) {
return clients.get(config.getAppId());
}
AipFace client = new AipFace(config.getAppId(), config.getApiKey(), config.getSecretKey());
client.setConnectionTimeoutInMillis(5000);
client.setSocketTimeoutInMillis(60000);
clients.put(config.getAppId(), client);
return client;
}
}
/**
* 下载图片并转换为base64字符串
*
* @param imageUrl 图片URL
* @return base64编码的图片字符串,失败返回null
*/
private String downloadImageAsBase64(String imageUrl) {
BufferedImage image = null;
ByteArrayOutputStream baos = null;
try {
// 下载图片
URL url = new URL(imageUrl.replace("oss-cn-shanghai.aliyuncs.com", "oss-cn-shanghai-internal.aliyuncs.com"));
image = ImageIO.read(url);
if (image == null) {
log.error("无法读取图片,URL: {}", imageUrl);
return null;
}
// 转换为字节数组
baos = new ByteArrayOutputStream();
String format = "jpg";
if (imageUrl.toLowerCase().endsWith(".png")) {
format = "png";
}
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
// 编码为base64
return Base64.encode(imageBytes);
} catch (IOException e) {
log.error("下载图片或转换base64失败,URL: {}", imageUrl, e);
return null;
} finally {
if (image != null) {
image.flush();
}
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
log.warn("关闭ByteArrayOutputStream失败", e);
}
}
}
}
private IRateLimiter getLimiter(LOCK_TYPE type) {
return switch (type) {
case ADD_DB ->
addDbLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
case ADD_FACE ->
addFaceLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(config.getAddQps()));
case LIST_DB ->
listDbLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
case LIST_FACE ->
listFaceLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
case SEARCH_FACE ->
searchFaceLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(config.getSearchQps()));
case DELETE_DB ->
deleteDbLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
case DELETE_ENTITY ->
deleteEntityLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
case DELETE_FACE ->
deleteFaceLimiters.computeIfAbsent(config.getAppId(), k -> new FixedRateLimiter(105, TimeUnit.MILLISECONDS));
default -> new FixedRateLimiter(500, TimeUnit.MILLISECONDS);
};
}
protected enum LOCK_TYPE {
ADD_DB,
ADD_FACE,
LIST_DB,
LIST_FACE,
SEARCH_FACE,
DELETE_DB,
DELETE_ENTITY,
DELETE_FACE,
}
}