diff --git a/pom.xml b/pom.xml index c7f41d1..fe62cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,11 @@ spring-boot-starter-test test + + junit + junit + test + org.springframework.boot @@ -181,6 +186,13 @@ aliyun-java-sdk-facebody 2.0.12 + + + + com.baidu.aip + java-sdk + 4.16.19 + diff --git a/src/main/java/com/ycwl/basic/facebody/FaceBodyFactory.java b/src/main/java/com/ycwl/basic/facebody/FaceBodyFactory.java index 811f79b..cbed1d9 100644 --- a/src/main/java/com/ycwl/basic/facebody/FaceBodyFactory.java +++ b/src/main/java/com/ycwl/basic/facebody/FaceBodyFactory.java @@ -1,6 +1,7 @@ package com.ycwl.basic.facebody; import com.ycwl.basic.facebody.adapter.AliFaceBodyAdapter; +import com.ycwl.basic.facebody.adapter.BceFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.enums.FaceBodyAdapterType; import com.ycwl.basic.facebody.exceptions.FaceBodyUnsupportedException; @@ -25,6 +26,8 @@ public class FaceBodyFactory { switch (type) { case ALI: return new AliFaceBodyAdapter(); + case BCE: + return new BceFaceBodyAdapter(); default: throw new FaceBodyUnsupportedException("不支持的Adapter类型"); } diff --git a/src/main/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapter.java b/src/main/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapter.java index a969fa4..98b8a05 100644 --- a/src/main/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapter.java +++ b/src/main/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapter.java @@ -24,6 +24,7 @@ import com.ycwl.basic.ratelimiter.FixedRateLimiter; import com.ycwl.basic.ratelimiter.IRateLimiter; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.Map; @@ -35,8 +36,9 @@ import java.util.stream.Collectors; public class AliFaceBodyAdapter implements IFaceBodyAdapter { private static final Map addEntityLimiters = new ConcurrentHashMap<>(); private static final Map addFaceLimiters = new ConcurrentHashMap<>(); - private static final Map searchFaceLimiters = new ConcurrentHashMap<>(); private static final Map addDbLimiters = new ConcurrentHashMap<>(); + private static final Map listDbLimiters = new ConcurrentHashMap<>(); + private static final Map searchFaceLimiters = new ConcurrentHashMap<>(); private static final Map deleteDbLimiters = new ConcurrentHashMap<>(); private static final Map deleteEntityLimiters = new ConcurrentHashMap<>(); @@ -74,6 +76,11 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter { addFaceLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS)); } return addFaceLimiters.get(config.getAccessKeyId()); + case LIST_DB: + if (listDbLimiters.get(config.getAccessKeyId()) == null) { + listDbLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(500, TimeUnit.MILLISECONDS)); + } + return listDbLimiters.get(config.getAccessKeyId()); case SEARCH_FACE: if (searchFaceLimiters.get(config.getAccessKeyId()) == null) { searchFaceLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(200, TimeUnit.MILLISECONDS)); @@ -244,7 +251,9 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter { } else { listFaceEntitiesRequest.setLimit(200); } - listFaceEntitiesRequest.setEntityIdPrefix(prefix); + if (StringUtils.isNotEmpty(prefix)) { + listFaceEntitiesRequest.setEntityIdPrefix(prefix); + } try (ClientWrapper clientWrapper = getClient()) { IAcsClient client = clientWrapper.getClient(); try { @@ -275,6 +284,7 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter { SearchFaceResponse response = client.getAcsResponse(request); List matchList = response.getData().getMatchList(); if (matchList.isEmpty()) { + resp.setOriginalFaceScore(0f); return resp; } SearchFaceResponse.Data.MatchListItem matchItem = matchList.get(0); @@ -326,6 +336,7 @@ public class AliFaceBodyAdapter implements IFaceBodyAdapter { ADD_DB, ADD_ENTITY, ADD_FACE, + LIST_DB, SEARCH_FACE, DELETE_DB, DELETE_ENTITY, diff --git a/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java b/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java new file mode 100644 index 0000000..57a5cf9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapter.java @@ -0,0 +1,377 @@ +package com.ycwl.basic.facebody.adapter; + +import com.baidu.aip.face.AipFace; +import com.ycwl.basic.facebody.entity.AddFaceResp; +import com.ycwl.basic.facebody.entity.AliFaceBodyConfig; +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 java.math.BigDecimal; +import java.math.RoundingMode; +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; +import java.util.stream.Collectors; + +@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<>(); + 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"); + try { + addEntityLimiter.acquire(); + } catch (InterruptedException ignored) { + } + JSONObject response = client.addUser(faceUrl, "URL", dbName, entityId, options); + if (response.getInt("error_code") == 0) { + AddFaceResp resp = new AddFaceResp(); + resp.setScore(100f); + return resp; + } 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); + 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) { + try { + AipFace client = getClient(); + HashMap options = new HashMap<>(); + JSONObject response = client.faceGetlist(entityId, dbName, options); + if (response.getInt("error_code") == 0) { + JSONObject resultObj = response.getJSONObject("result"); + if (resultObj != null) { + 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; + } else { + return Collections.emptyList(); + } + } else { + log.warn("获取人脸列表失败!{}", response); + return Collections.emptyList(); + } + } catch (Exception e) { + log.error("获取人脸列表失败!", e); + return Collections.emptyList(); + } + } + + @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("quality_control", "LOW"); + options.put("max_user_num", "50"); + try { + searchFaceLimiter.acquire(); + } catch (InterruptedException ignored) { + } + JSONObject response = client.search(faceUrl, "URL", dbName, options); + if (response.getInt("error_code") == 0) { + resp.setOriginalFaceScore(100f); + JSONArray userList = response.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.get(0).getScore()); + } + return resp; + } else { + 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; + } + } + + private IRateLimiter getLimiter(LOCK_TYPE type) { + switch (type) { + case ADD_DB: + if (addDbLimiters.get(config.getAppId()) == null) { + addDbLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return addDbLimiters.get(config.getAppId()); + case ADD_FACE: + if (addFaceLimiters.get(config.getAppId()) == null) { + addFaceLimiters.put(config.getAppId(), new FixedRateLimiter(config.getAddQps())); + } + return addFaceLimiters.get(config.getAppId()); + case LIST_DB: + if (listDbLimiters.get(config.getAppId()) == null) { + listDbLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return listDbLimiters.get(config.getAppId()); + case LIST_FACE: + if (listFaceLimiters.get(config.getAppId()) == null) { + listFaceLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return listFaceLimiters.get(config.getAppId()); + case SEARCH_FACE: + if (searchFaceLimiters.get(config.getAppId()) == null) { + searchFaceLimiters.put(config.getAppId(), new FixedRateLimiter(config.getSearchQps())); + } + return searchFaceLimiters.get(config.getAppId()); + case DELETE_DB: + if (deleteDbLimiters.get(config.getAppId()) == null) { + deleteDbLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return deleteDbLimiters.get(config.getAppId()); + case DELETE_ENTITY: + if (deleteEntityLimiters.get(config.getAppId()) == null) { + deleteEntityLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return deleteEntityLimiters.get(config.getAppId()); + case DELETE_FACE: + if (deleteFaceLimiters.get(config.getAppId()) == null) { + deleteFaceLimiters.put(config.getAppId(), new FixedRateLimiter(100, TimeUnit.MILLISECONDS)); + } + return deleteFaceLimiters.get(config.getAppId()); + default: + return 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, + } +} diff --git a/src/main/java/com/ycwl/basic/facebody/entity/BceFaceBodyConfig.java b/src/main/java/com/ycwl/basic/facebody/entity/BceFaceBodyConfig.java new file mode 100644 index 0000000..c03d743 --- /dev/null +++ b/src/main/java/com/ycwl/basic/facebody/entity/BceFaceBodyConfig.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.facebody.entity; + +import lombok.Data; + +@Data +public class BceFaceBodyConfig { + private String appId; + private String apiKey; + private String secretKey; + private float addQps = 2.0f; + private float searchQps = 2.0f; +} diff --git a/src/main/java/com/ycwl/basic/facebody/enums/FaceBodyAdapterType.java b/src/main/java/com/ycwl/basic/facebody/enums/FaceBodyAdapterType.java index 5cbd180..7698334 100644 --- a/src/main/java/com/ycwl/basic/facebody/enums/FaceBodyAdapterType.java +++ b/src/main/java/com/ycwl/basic/facebody/enums/FaceBodyAdapterType.java @@ -4,7 +4,8 @@ import lombok.Getter; @Getter public enum FaceBodyAdapterType { - ALI("ALI") + ALI("ALI"), + BCE("BCE"), ; private final String code; diff --git a/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java b/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java index f975c75..acb0da0 100644 --- a/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java +++ b/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java @@ -9,6 +9,16 @@ public class FixedRateLimiter implements IRateLimiter { private final Semaphore semaphore = new Semaphore(1); private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); + public FixedRateLimiter(float maxRequestsPerSecond) { + int rate = Float.valueOf(1000000 / maxRequestsPerSecond).intValue(); + scheduler.scheduleAtFixedRate(() -> { + if (semaphore.availablePermits() < 1) { + semaphore.release(1); + } + }, rate, rate, TimeUnit.NANOSECONDS); + } + + public FixedRateLimiter(int rate, TimeUnit timeUnit) { // 启动一个线程每0.5秒释放一个许可 scheduler.scheduleAtFixedRate(() -> { diff --git a/src/test/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapterTest.java b/src/test/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapterTest.java new file mode 100644 index 0000000..0bf4693 --- /dev/null +++ b/src/test/java/com/ycwl/basic/facebody/adapter/BceFaceBodyAdapterTest.java @@ -0,0 +1,49 @@ +package com.ycwl.basic.facebody.adapter; + +import com.ycwl.basic.facebody.entity.BceFaceBodyConfig; +import org.junit.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class BceFaceBodyAdapterTest { + private BceFaceBodyAdapter getAdapter() { + BceFaceBodyAdapter adapter = new BceFaceBodyAdapter(); + BceFaceBodyConfig config = new BceFaceBodyConfig(); + config.setAppId("118363478"); + config.setApiKey("3rXrDdU4cHZqLS8ICFSYZKse"); + config.setSecretKey("zgGFehERZKYXEiRQpqWs9AYchLxzXzYa"); + adapter.setConfig(config); + return adapter; + } + + @Test + public void testDbCreate() { + BceFaceBodyAdapter adapter = getAdapter(); + boolean b = adapter.addFaceDb("test"); + assertTrue(b); + boolean b0 = adapter.assureFaceDb("test"); + assertTrue(b0); + boolean b1 = adapter.deleteFaceDb("test"); + assertTrue(b1); + boolean b2 = adapter.assureFaceDb("test"); + assertTrue(b2); + boolean b3 = adapter.deleteFaceDb("test"); + assertTrue(b3); + } + + @Test + public void testAddFace() { + BceFaceBodyAdapter adapter = getAdapter(); + adapter.assureFaceDb("test"); + adapter.addFace("test", "test", "https://frametour-assets.oss-cn-shanghai.aliyuncs.com/user-faces/user-face/c925d970-216a-4eff-b699-cd047c0f9088.jpg", "test"); + List strings = adapter.listUserFace("test", "test"); + assertFalse(strings.isEmpty()); + adapter.deleteFace("test", "test"); + List stringList = adapter.listUserFace("test", "test"); + assertTrue(stringList.isEmpty()); + List listFace = adapter.listFace("test", null, 0, 10); + assertTrue(listFace.isEmpty()); + } +} \ No newline at end of file