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 clients = new ConcurrentHashMap<>(); private static final Map addEntityLimiters = new ConcurrentHashMap<>(); private static final Map addFaceLimiters = new ConcurrentHashMap<>(); private static final Map addDbLimiters = new ConcurrentHashMap<>(); private static final Map listDbLimiters = new ConcurrentHashMap<>(); private static final Map listFaceLimiters = new ConcurrentHashMap<>(); private static final Map searchFaceLimiters = new ConcurrentHashMap<>(); private static final Map deleteDbLimiters = new ConcurrentHashMap<>(); private static final Map deleteEntityLimiters = new ConcurrentHashMap<>(); private static final Map 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 _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 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 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 listFaceDb() { IRateLimiter listDbLimiter = getLimiter(LOCK_TYPE.LIST_DB); try { AipFace client = getClient(); HashMap 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 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 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 options = new HashMap<>(); List 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 listFace(String dbName, String prefix, Integer offset, Integer size) { IRateLimiter listFaceLimiter = getLimiter(LOCK_TYPE.LIST_FACE); try { AipFace client = getClient(); HashMap 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 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 listUserFace(String dbName, String entityId) { IRateLimiter listFaceLimiter = getLimiter(LOCK_TYPE.LIST_FACE); try { AipFace client = getClient(); HashMap 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 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 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 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 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, } }