You've already forked FrameTour-BE
- 新增账号级别调度器管理器,支持多账号QPS隔离控制 - 为阿里云和百度云适配器添加配置getter方法 - 移除原有阻塞式限流逻辑,交由外层调度器统一管控 - 创建QPS调度器实现精确的任务频率控制 - 新增监控接口用于查询各账号调度器运行状态 - 重构人脸识别Kafka消费服务,集成账号调度机制 - 优化线程池资源配置,提升多账号并发处理效率 - 增强错误处理与状态更新的安全性 - 删除旧版全局线程池配置类 - 完善任务提交与状态流转的日志记录
489 lines
21 KiB
Java
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,
|
|
}
|
|
}
|