This commit is contained in:
Jerry Yan 2025-04-05 13:21:41 +08:00
parent 36f1242e79
commit 67dca0d4d4
15 changed files with 579 additions and 13 deletions

View File

@ -0,0 +1,59 @@
package com.ycwl.basic.facebody;
import com.ycwl.basic.facebody.adapter.AliFaceBodyAdapter;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
import com.ycwl.basic.facebody.exceptions.FaceBodyUnsupportedException;
import com.ycwl.basic.storage.exceptions.StorageConfigException;
import com.ycwl.basic.storage.exceptions.StorageUndefinedException;
import java.util.HashMap;
import java.util.Map;
public class FaceBodyFactory {
public static IFaceBodyAdapter getAdapter(String typeName) {
FaceBodyAdapterType adapterEnum;
try {
adapterEnum = FaceBodyAdapterType.valueOf(typeName);
} catch (Exception e) {
throw new FaceBodyUnsupportedException("不支持的Adapter类型");
}
return getAdapter(adapterEnum);
}
public static IFaceBodyAdapter getAdapter(FaceBodyAdapterType type) {
switch (type) {
case ALI:
return new AliFaceBodyAdapter();
default:
throw new FaceBodyUnsupportedException("不支持的Adapter类型");
}
}
protected static Map<String, IFaceBodyAdapter> namedAdapter = new HashMap<>();
protected static IFaceBodyAdapter defaultAdapter = null;
public static void register(String name, IFaceBodyAdapter adapter) {
namedAdapter.put(name, adapter);
}
public static IFaceBodyAdapter use(String name) {
IFaceBodyAdapter adapter = namedAdapter.get(name);
if (adapter == null) {
throw new StorageUndefinedException("未定义的存储方式:"+name);
}
return adapter;
}
public static IFaceBodyAdapter use() {
if (defaultAdapter == null) {
throw new StorageConfigException("未定义默认存储方式");
}
return defaultAdapter;
}
public static void setDefault(String defaultName) {
FaceBodyFactory.defaultAdapter = use(defaultName);
}
}

View File

@ -0,0 +1,333 @@
package com.ycwl.basic.facebody.adapter;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.facebody.model.v20191230.AddFaceEntityRequest;
import com.aliyuncs.facebody.model.v20191230.AddFaceRequest;
import com.aliyuncs.facebody.model.v20191230.AddFaceResponse;
import com.aliyuncs.facebody.model.v20191230.CreateFaceDbRequest;
import com.aliyuncs.facebody.model.v20191230.DeleteFaceDbRequest;
import com.aliyuncs.facebody.model.v20191230.DeleteFaceEntityRequest;
import com.aliyuncs.facebody.model.v20191230.ListFaceDbsRequest;
import com.aliyuncs.facebody.model.v20191230.ListFaceDbsResponse;
import com.aliyuncs.facebody.model.v20191230.ListFaceEntitiesRequest;
import com.aliyuncs.facebody.model.v20191230.ListFaceEntitiesResponse;
import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest;
import com.aliyuncs.facebody.model.v20191230.SearchFaceResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.facebody.entity.AliFaceBodyConfig;
import com.ycwl.basic.facebody.entity.SearchFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.ratelimiter.FixedRateLimiter;
import com.ycwl.basic.ratelimiter.IRateLimiter;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
public class AliFaceBodyAdapter implements IFaceBodyAdapter {
private static final Map<String, IRateLimiter> addEntityLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> addFaceLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> searchFaceLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> addDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteDbLimiters = new ConcurrentHashMap<>();
private static final Map<String, IRateLimiter> deleteEntityLimiters = new ConcurrentHashMap<>();
private AliFaceBodyConfig config;
public boolean setConfig(AliFaceBodyConfig config) {
this.config = config;
return true;
}
@Override
public boolean loadConfig(Map<String, String> _config) {
AliFaceBodyConfig config = new AliFaceBodyConfig();
config.setAccessKeyId(_config.get("accessKeyId"));
config.setAccessKeySecret(_config.get("accessKeySecret"));
config.setRegion(_config.get("region"));
this.config = config;
return true;
}
private IRateLimiter getLimiter(LOCK_TYPE type) {
switch (type) {
case ADD_DB:
if (addDbLimiters.get(config.getAccessKeyId()) == null) {
addDbLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS));
}
return addDbLimiters.get(config.getAccessKeyId());
case ADD_ENTITY:
if (addEntityLimiters.get(config.getAccessKeyId()) == null) {
addEntityLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS));
}
return addEntityLimiters.get(config.getAccessKeyId());
case ADD_FACE:
if (addFaceLimiters.get(config.getAccessKeyId()) == null) {
addFaceLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS));
}
return addFaceLimiters.get(config.getAccessKeyId());
case SEARCH_FACE:
if (searchFaceLimiters.get(config.getAccessKeyId()) == null) {
searchFaceLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(200, TimeUnit.MILLISECONDS));
}
return searchFaceLimiters.get(config.getAccessKeyId());
case DELETE_DB:
if (deleteDbLimiters.get(config.getAccessKeyId()) == null) {
deleteDbLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS));
}
return deleteDbLimiters.get(config.getAccessKeyId());
case DELETE_ENTITY:
if (deleteEntityLimiters.get(config.getAccessKeyId()) == null) {
deleteEntityLimiters.put(config.getAccessKeyId(), new FixedRateLimiter(600, TimeUnit.MILLISECONDS));
}
return deleteEntityLimiters.get(config.getAccessKeyId());
default:
return new FixedRateLimiter(600, TimeUnit.MILLISECONDS);
}
}
@Override
public boolean addFaceDb(String dbName) {
IRateLimiter addDbLimiter = getLimiter(LOCK_TYPE.ADD_DB);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
CreateFaceDbRequest request = new CreateFaceDbRequest();
request.setName(dbName);
try {
addDbLimiter.acquire();
} catch (InterruptedException ignored) {
}
client.getAcsResponse(request);
return true;
} catch (ClientException e) {
log.error("阿里云添加人脸数据库失败!", e);
return false;
}
}
@Override
public boolean deleteFaceDb(String dbName) {
ListFaceEntitiesRequest request = new ListFaceEntitiesRequest();
IRateLimiter deleteEntityLimiter = getLimiter(LOCK_TYPE.DELETE_ENTITY);
IRateLimiter deleteDbLimiter = getLimiter(LOCK_TYPE.DELETE_DB);
request.setDbName(dbName);
request.setLimit(200);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
while (true) {
ListFaceEntitiesResponse response = client.getAcsResponse(request);
if (response.getData().getTotalCount() == 0) {
break;
}
response.getData().getEntities().forEach(entity -> {
DeleteFaceEntityRequest deleteFaceEntityRequest = new DeleteFaceEntityRequest();
deleteFaceEntityRequest.setDbName(entity.getDbName());
deleteFaceEntityRequest.setEntityId(entity.getEntityId());
try {
deleteEntityLimiter.acquire();
} catch (InterruptedException ignored) {
}
try {
client.getAcsResponse(deleteFaceEntityRequest);
} catch (ClientException e) {
log.error("删除人脸数据失败!", e);
}
});
}
DeleteFaceDbRequest deleteFaceDbRequest = new DeleteFaceDbRequest();
deleteFaceDbRequest.setName(dbName);
try {
deleteDbLimiter.acquire();
} catch (InterruptedException ignored) {
}
client.getAcsResponse(deleteFaceDbRequest);
} catch (ClientException e) {
log.error("删除人脸数据库失败!", e);
return false;
}
return true;
}
@Override
public List<String> listFaceDb() {
ListFaceDbsRequest request = new ListFaceDbsRequest();
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
ListFaceDbsResponse response = client.getAcsResponse(request);
return response.getData().getDbList().stream().map(ListFaceDbsResponse.Data.DbListItem::getName).collect(Collectors.toList());
} catch (ClientException e) {
log.error("获取人脸数据库失败!", e);
return null;
}
}
@Override
public AddFaceResp addFace(String dbName, String entityId, String faceUrl, String extData) {
IRateLimiter addEntityLimiter = getLimiter(LOCK_TYPE.ADD_ENTITY);
IRateLimiter addFaceLimiter = getLimiter(LOCK_TYPE.ADD_FACE);
AddFaceEntityRequest request = new AddFaceEntityRequest();
request.setDbName(dbName);
request.setEntityId(entityId);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
try {
addEntityLimiter.acquire();
} catch (InterruptedException ignored) {
}
try {
client.getAcsResponse(request);
} catch (ClientException e) {
log.error("addFaceEntity, {}/{}", dbName, entityId, e);
return null;
}
AddFaceRequest addFaceRequest = new AddFaceRequest();
addFaceRequest.setDbName(dbName);
addFaceRequest.setEntityId(entityId);
addFaceRequest.setImageUrl(faceUrl);
addFaceRequest.setExtraData(extData);
AddFaceResp respVo = new AddFaceResp();
try {
addFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
try {
AddFaceResponse acsResponse = client.getAcsResponse(addFaceRequest);
respVo.setScore(acsResponse.getData().getQualitieScore());
return respVo;
} catch (ClientException e) {
log.error("addFace, {}/{}", dbName, entityId, e);
return null;
}
}
}
@Override
public boolean deleteFace(String dbName, String entityId) {
IRateLimiter deleteEntityLimiter = getLimiter(LOCK_TYPE.DELETE_ENTITY);
DeleteFaceEntityRequest request = new DeleteFaceEntityRequest();
request.setDbName(dbName);
request.setEntityId(entityId);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
try {
deleteEntityLimiter.acquire();
} catch (InterruptedException ignored) {
}
try {
client.getAcsResponse(request);
return true;
} catch (ClientException e) {
log.error("删除人脸数据失败!", e);
return false;
}
}
}
@Override
public List<String> listFace(String dbName, String prefix, Integer offset, Integer size) {
ListFaceEntitiesRequest listFaceEntitiesRequest = new ListFaceEntitiesRequest();
listFaceEntitiesRequest.setDbName(dbName);
listFaceEntitiesRequest.setOrder("asc");
if (offset != null) {
listFaceEntitiesRequest.setOffset(offset);
}
if (size != null) {
listFaceEntitiesRequest.setLimit(size);
} else {
listFaceEntitiesRequest.setLimit(200);
}
listFaceEntitiesRequest.setEntityIdPrefix(prefix);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
try {
ListFaceEntitiesResponse response = client.getAcsResponse(listFaceEntitiesRequest);
return response.getData().getEntities().stream().map(ListFaceEntitiesResponse.Data.Entity::getEntityId).collect(Collectors.toList());
} catch (ClientException e) {
log.error("获取人脸数据失败!", e);
return null;
}
}
}
@Override
public SearchFaceResp searchFace(String dbName, String faceUrl) {
SearchFaceResp resp = new SearchFaceResp();
IRateLimiter searchFaceLimiter = getLimiter(LOCK_TYPE.SEARCH_FACE);
try (ClientWrapper clientWrapper = getClient()) {
IAcsClient client = clientWrapper.getClient();
SearchFaceRequest request = new SearchFaceRequest();
request.setDbName(dbName);
request.setImageUrl(faceUrl);
request.setLimit(100);
try {
searchFaceLimiter.acquire();
} catch (InterruptedException ignored) {
}
try {
SearchFaceResponse response = client.getAcsResponse(request);
List<SearchFaceResponse.Data.MatchListItem> matchList = response.getData().getMatchList();
if (matchList.isEmpty()) {
return resp;
}
SearchFaceResponse.Data.MatchListItem matchItem = matchList.get(0);
resp.setOriginalFaceScore(matchItem.getQualitieScore());
resp.setResult(matchItem.getFaceItems().stream().map(item -> {
SearchFaceResultItem resultItem = new SearchFaceResultItem();
resultItem.setDbName(dbName);
resultItem.setFaceId(item.getFaceId());
resultItem.setExtData(item.getExtraData());
resultItem.setScore(item.getScore());
return resultItem;
}).collect(Collectors.toList()));
if (!resp.getResult().isEmpty()) {
resp.setFirstMatchRate(resp.getResult().get(0).getScore());
}
return resp;
} catch (ClientException e) {
log.error("搜索人脸失败!", e);
return null;
}
}
}
public ClientWrapper getClient() {
DefaultProfile profile = DefaultProfile.getProfile(
config.getRegion(), config.getAccessKeyId(), config.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
return new ClientWrapper(client);
}
@Getter
public static class ClientWrapper implements AutoCloseable {
private final IAcsClient client;
public ClientWrapper(IAcsClient client) {
this.client = client;
}
@Override
public void close() {
if (client == null) {
return;
}
client.shutdown();
}
}
protected enum LOCK_TYPE {
ADD_DB,
ADD_ENTITY,
ADD_FACE,
SEARCH_FACE,
DELETE_DB,
DELETE_ENTITY,
}
}

View File

@ -0,0 +1,29 @@
package com.ycwl.basic.facebody.adapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.facebody.entity.SearchFaceResp;
import java.util.List;
import java.util.Map;
public interface IFaceBodyAdapter {
boolean loadConfig(Map<String, String> _config);
default boolean assureFaceDb(String dbName) {
List<String> faceDbs = listFaceDb();
return faceDbs.contains(dbName) || addFaceDb(dbName);
}
boolean addFaceDb(String dbName);
boolean deleteFaceDb(String dbName);
List<String> listFaceDb();
AddFaceResp addFace(String dbName, String entityId, String faceUrl, String extData);
boolean deleteFace(String dbName, String entityId);
List<String> listFace(String dbName, String prefix, Integer offset, Integer size);
SearchFaceResp searchFace(String dbName, String faceUrl);
}

View File

@ -0,0 +1,8 @@
package com.ycwl.basic.facebody.entity;
import lombok.Data;
@Data
public class AddFaceResp {
private Float score;
}

View File

@ -0,0 +1,10 @@
package com.ycwl.basic.facebody.entity;
import lombok.Data;
@Data
public class AliFaceBodyConfig {
private String accessKeyId;
private String accessKeySecret;
private String region;
}

View File

@ -0,0 +1,13 @@
package com.ycwl.basic.facebody.entity;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class SearchFaceResp {
private Float originalFaceScore;
private List<SearchFaceResultItem> result = new ArrayList<>();
private Float firstMatchRate;
}

View File

@ -0,0 +1,12 @@
package com.ycwl.basic.facebody.entity;
import lombok.Data;
@Data
public class SearchFaceResultItem {
private String dbName;
private String faceId;
private String extData;
/** 置信度0~1 */
private Float score;
}

View File

@ -0,0 +1,17 @@
package com.ycwl.basic.facebody.enums;
import lombok.Getter;
@Getter
public enum FaceBodyAdapterType {
ALI("ALI")
;
private final String code;
FaceBodyAdapterType(String code) {
this.code = code;
}
}

View File

@ -0,0 +1,7 @@
package com.ycwl.basic.facebody.exceptions;
public class FaceBodyException extends RuntimeException {
public FaceBodyException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.ycwl.basic.facebody.exceptions;
public class FaceBodyUnsupportedException extends RuntimeException {
public FaceBodyUnsupportedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,32 @@
package com.ycwl.basic.facebody.starter;
import com.ycwl.basic.facebody.FaceBodyFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.starter.config.FaceBodyConfig;
import com.ycwl.basic.facebody.starter.config.OverallFaceBodyConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FaceBodyAutoConfiguration {
private final OverallFaceBodyConfig config;
public FaceBodyAutoConfiguration(OverallFaceBodyConfig config) {
this.config = config;
if (config != null) {
if (config.getConfigs() != null) {
loadConfig();
}
if (StringUtils.isNotBlank(config.getDefaultUse())) {
FaceBodyFactory.setDefault(config.getDefaultUse());
}
}
}
private void loadConfig() {
for (FaceBodyConfig item : config.getConfigs()) {
IFaceBodyAdapter adapter = FaceBodyFactory.getAdapter(item.getType());
adapter.loadConfig(item.getConfig());
FaceBodyFactory.register(item.getName(), adapter);
}
}
}

View File

@ -0,0 +1,13 @@
package com.ycwl.basic.facebody.starter.config;
import com.ycwl.basic.facebody.enums.FaceBodyAdapterType;
import lombok.Data;
import java.util.Map;
@Data
public class FaceBodyConfig {
private String name;
private FaceBodyAdapterType type;
private Map<String, String> config;
}

View File

@ -0,0 +1,15 @@
package com.ycwl.basic.facebody.starter.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "facebody")
@Data
public class OverallFaceBodyConfig {
private String defaultUse;
private List<FaceBodyConfig> configs;
}

View File

@ -158,11 +158,16 @@ storage:
prefix: "user-video/"
url: "https://wsaiphoto.obs-cq.cucloud.cn"
region: "obs-cq"
#阿里云人脸检测
aliFace:
accessKeyId: "LTAI5tMwrmxVcUEKoH5QzLHx"
accessKeySecret: "ZCIP8aKx1jwX1wkeYIPQEDZ8fPtN1c"
region: "cn-shanghai"
#人脸检测
facebody:
default-use: "zt"
configs:
- name: "zt"
type: ALI
config:
accessKeyId: "LTAI5tMwrmxVcUEKoH5QzLHx"
accessKeySecret: "ZCIP8aKx1jwX1wkeYIPQEDZ8fPtN1c"
region: "cn-shanghai"
notify:
defaultUse: ""

View File

@ -34,15 +34,16 @@ spring:
port: 6379
# 密码过于复杂需要使用''引起来,要不可能导致项目无法启动,因为无法识别特殊字符
password: ''
timeout: 1000
timeout: 2000
# 配置用户头像存放静态资源文件夹
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
# 配置请求文件大小
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
web:
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
# MyBatis
mybatis-plus:
@ -160,11 +161,16 @@ storage:
url: "https://wsaiphoto.obs-cq.cucloud.cn"
region: "obs-cq"
#阿里云人脸检测
aliFace:
accessKeyId: "LTAI5tMwrmxVcUEKoH5QzLHx"
accessKeySecret: "ZCIP8aKx1jwX1wkeYIPQEDZ8fPtN1c"
region: "cn-shanghai"
#人脸检测
facebody:
default-use: "zt"
configs:
- name: "zt"
type: ALI
config:
accessKeyId: "LTAI5tMwrmxVcUEKoH5QzLHx"
accessKeySecret: "ZCIP8aKx1jwX1wkeYIPQEDZ8fPtN1c"
region: "cn-shanghai"
# 通知到人
notify: