From 00890c764ea76e81facaa8623fb65420379f1166 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 17 Dec 2025 15:28:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(basic):=20=E6=B7=BB=E5=8A=A0=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=89=87=E6=AE=B5=E6=9B=B4=E6=96=B0=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在FaceStatusManager中新增按模板ID区分的人脸片段更新状态缓存键 - 更新TaskTaskServiceImpl以设置模板渲染状态 - 在任务回调逻辑中增加对模板渲染状态的更新操作 - 修改任务删除逻辑为更新状态加10的临时解决方案 - 移除旧有的切割任务状态更新逻辑,统一使用模板渲染状态管理 --- .../com/ycwl/basic/biz/FaceStatusManager.java | 348 ++++++++++++++++++ .../com/ycwl/basic/biz/TaskStatusBiz.java | 192 ---------- .../controller/mobile/AppGoodsController.java | 1 - .../com/ycwl/basic/enums/FaceCutStatus.java | 65 ++++ .../basic/enums/FacePieceUpdateStatus.java | 64 ++++ .../basic/enums/TemplateRenderStatus.java | 75 ++++ .../com/ycwl/basic/enums/VideoTaskStatus.java | 159 ++++++++ .../face/pipeline/stages/CreateTaskStage.java | 7 +- .../pipeline/stages/SetTaskStatusStage.java | 7 +- .../service/mobile/impl/GoodsServiceImpl.java | 317 ++++++++++------ .../service/pc/impl/FaceServiceImpl.java | 2 +- .../FaceMatchingOrchestrator.java | 14 +- .../task/impl/TaskTaskServiceImpl.java | 32 +- .../com/ycwl/basic/task/VideoPieceGetter.java | 61 +-- .../pipeline/stages/CreateTaskStageTest.java | 15 +- .../stages/SetTaskStatusStageTest.java | 32 +- .../adapter/AliFaceBodyAdapterTest.java | 4 +- 17 files changed, 994 insertions(+), 401 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/biz/FaceStatusManager.java delete mode 100644 src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java create mode 100644 src/main/java/com/ycwl/basic/enums/FaceCutStatus.java create mode 100644 src/main/java/com/ycwl/basic/enums/FacePieceUpdateStatus.java create mode 100644 src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java create mode 100644 src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java diff --git a/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java b/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java new file mode 100644 index 00000000..650ba991 --- /dev/null +++ b/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java @@ -0,0 +1,348 @@ +package com.ycwl.basic.biz; + +import com.ycwl.basic.enums.FaceCutStatus; +import com.ycwl.basic.enums.FacePieceUpdateStatus; +import com.ycwl.basic.enums.TemplateRenderStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 人脸状态缓存管理器 + * 统一管理人脸相关的Redis缓存状态 + */ +@Slf4j +@Component +public class FaceStatusManager { + + /** + * 人脸切片状态缓存Key + * 格式: face:status:cut:{faceId} + * 过期时间: 1天 + */ + private static final String FACE_CUT_STATUS_KEY = "face:status:cut:%s"; + + /** + * 人脸片段更新状态缓存Key(全局) + * 格式: face:status:piece:update:{faceId} + * 过期时间: 1天 + * 键存在 = 无新片段,键不存在 = 有新片段 + */ + private static final String FACE_PIECE_UPDATE_KEY = "face:status:piece:update:%s"; + + /** + * 人脸模板片段更新状态缓存Key(按模板) + * 格式: face:status:piece:update:{faceId}:{templateId} + * 过期时间: 1天 + * 键存在 = 无新片段,键不存在 = 有新片段 + */ + private static final String FACE_TEMPLATE_PIECE_UPDATE_KEY = "face:status:piece:update:%s:%s"; + + /** + * 人脸模板渲染状态缓存Key + * 格式: face:status:render:{faceId}:{templateId} + * 过期时间: 永久(或根据业务需要设置) + */ + private static final String FACE_TEMPLATE_RENDER_KEY = "face:status:render:%s:%s"; + + /** + * 默认过期时间:1天 + */ + private static final long DEFAULT_EXPIRE_SECONDS = 86400L; + + @Autowired + private RedisTemplate redisTemplate; + + // ==================== 切片状态相关方法 ==================== + + /** + * 设置人脸切片状态 + * @param faceId 人脸ID + * @param status 切片状态 + */ + public void setFaceCutStatus(Long faceId, FaceCutStatus status) { + if (faceId == null || status == null) { + log.warn("设置切片状态参数为空: faceId={}, status={}", faceId, status); + return; + } + String key = String.format(FACE_CUT_STATUS_KEY, faceId); + redisTemplate.opsForValue().set(key, String.valueOf(status.getCode()), DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS); + log.debug("设置切片状态: faceId={}, status={}", faceId, status.getDescription()); + } + + /** + * 获取人脸切片状态 + * @param faceId 人脸ID + * @return 切片状态,缓存不存在时返回 COMPLETED(已完成) + */ + public FaceCutStatus getFaceCutStatus(Long faceId) { + if (faceId == null) { + log.warn("获取切片状态参数为空: faceId={}", faceId); + return FaceCutStatus.COMPLETED; + } + String key = String.format(FACE_CUT_STATUS_KEY, faceId); + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + log.debug("切片状态缓存不存在,返回默认值COMPLETED: faceId={}", faceId); + return FaceCutStatus.COMPLETED; + } + try { + int code = Integer.parseInt(value); + return FaceCutStatus.fromCodeOrDefault(code, FaceCutStatus.COMPLETED); + } catch (NumberFormatException e) { + log.error("切片状态值解析失败: faceId={}, value={}", faceId, value, e); + return FaceCutStatus.COMPLETED; + } + } + + /** + * 删除人脸切片状态缓存 + * @param faceId 人脸ID + */ + public void deleteFaceCutStatus(Long faceId) { + if (faceId == null) { + return; + } + String key = String.format(FACE_CUT_STATUS_KEY, faceId); + redisTemplate.delete(key); + log.debug("删除切片状态缓存: faceId={}", faceId); + } + + // ==================== 片段更新状态相关方法 ==================== + + /** + * 标记无新片段(设置Redis键) + * @param faceId 人脸ID + * @param templateId 模板ID(可选,为null时标记全局状态) + */ + public void markNoNewPieces(Long faceId, Long templateId) { + if (faceId == null) { + log.warn("标记无新片段参数为空: faceId={}", faceId); + return; + } + + String key; + if (templateId == null) { + // 全局标记:该人脸的所有模板都无新片段 + key = String.format(FACE_PIECE_UPDATE_KEY, faceId); + log.debug("标记无新片段(全局): faceId={}", faceId); + } else { + // 模板级标记:该人脸在该模板下无新片段 + key = String.format(FACE_TEMPLATE_PIECE_UPDATE_KEY, faceId, templateId); + log.debug("标记无新片段(模板): faceId={}, templateId={}", faceId, templateId); + } + + redisTemplate.opsForValue().set(key, "1", DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + + /** + * 标记有新片段(删除Redis键) + * @param faceId 人脸ID + * @param templateId 模板ID(可选,为null时标记全局状态) + */ + public void markHasNewPieces(Long faceId, Long templateId) { + if (faceId == null) { + log.warn("标记有新片段参数为空: faceId={}", faceId); + return; + } + + String key; + if (templateId == null) { + // 全局标记:该人脸有新片段 + key = String.format(FACE_PIECE_UPDATE_KEY, faceId); + log.debug("标记有新片段(全局): faceId={}", faceId); + } else { + // 模板级标记:该人脸在该模板下有新片段 + key = String.format(FACE_TEMPLATE_PIECE_UPDATE_KEY, faceId, templateId); + log.debug("标记有新片段(模板): faceId={}, templateId={}", faceId, templateId); + } + + redisTemplate.delete(key); + } + + /** + * 获取人脸片段更新状态 + * @param faceId 人脸ID + * @param templateId 模板ID(可选,为null时查询全局状态) + * @return 片段更新状态,键存在=无新片段,键不存在=有新片段 + */ + public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId, Long templateId) { + if (faceId == null) { + log.warn("获取片段更新状态参数为空: faceId={}", faceId); + return FacePieceUpdateStatus.HAS_NEW_PIECES; + } + + String key; + if (templateId == null) { + // 查询全局状态 + key = String.format(FACE_PIECE_UPDATE_KEY, faceId); + } else { + // 查询模板级状态 + key = String.format(FACE_TEMPLATE_PIECE_UPDATE_KEY, faceId, templateId); + } + + Boolean exists = redisTemplate.hasKey(key); + FacePieceUpdateStatus status = FacePieceUpdateStatus.fromKeyExists(Boolean.TRUE.equals(exists)); + + if (templateId == null) { + log.debug("获取片段更新状态(全局): faceId={}, status={}", faceId, status.getDescription()); + } else { + log.debug("获取片段更新状态(模板): faceId={}, templateId={}, status={}", + faceId, templateId, status.getDescription()); + } + + return status; + } + + /** + * 获取人脸片段更新状态 - 全局版本 + * @param faceId 人脸ID + * @return 片段更新状态,键存在=无新片段,键不存在=有新片段 + */ + public FacePieceUpdateStatus getFacePieceUpdateStatus(Long faceId) { + return getFacePieceUpdateStatus(faceId, null); + } + + /** + * 判断是否有新片段 + * @param faceId 人脸ID + * @param templateId 模板ID(可选,为null时查询全局状态) + * @return true=有新片段,false=无新片段;如果templateId为null则默认返回true(有新片段) + */ + public boolean hasNewPieces(Long faceId, Long templateId) { + if (templateId == null) { + // 如果没有指定templateId,默认认为有新片段 + log.debug("未指定templateId,默认返回有新片段: faceId={}", faceId); + return true; + } + return getFacePieceUpdateStatus(faceId, templateId).hasNewPieces(); + } + + /** + * 判断是否有新片段 - 全局版本 + * @param faceId 人脸ID + * @return true=有新片段,false=无新片段 + */ + public boolean hasNewPieces(Long faceId) { + return getFacePieceUpdateStatus(faceId, null).hasNewPieces(); + } + + // ==================== 模板渲染状态相关方法 ==================== + + /** + * 设置人脸模板渲染状态 + * @param faceId 人脸ID + * @param templateId 模板ID + * @param status 渲染状态 + */ + public void setTemplateRenderStatus(Long faceId, Long templateId, TemplateRenderStatus status) { + if (faceId == null || templateId == null || status == null) { + log.warn("设置模板渲染状态参数为空: faceId={}, templateId={}, status={}", faceId, templateId, status); + return; + } + String key = String.format(FACE_TEMPLATE_RENDER_KEY, faceId, templateId); + // 渲染状态不设置过期时间,永久保存 + redisTemplate.opsForValue().set(key, String.valueOf(status.getCode())); + log.debug("设置模板渲染状态: faceId={}, templateId={}, status={}", faceId, templateId, status.getDescription()); + } + + /** + * 设置人脸模板渲染状态(带过期时间) + * @param faceId 人脸ID + * @param templateId 模板ID + * @param status 渲染状态 + * @param expireSeconds 过期时间(秒) + */ + public void setTemplateRenderStatus(Long faceId, Long templateId, TemplateRenderStatus status, long expireSeconds) { + if (faceId == null || templateId == null || status == null) { + log.warn("设置模板渲染状态参数为空: faceId={}, templateId={}, status={}", faceId, templateId, status); + return; + } + String key = String.format(FACE_TEMPLATE_RENDER_KEY, faceId, templateId); + redisTemplate.opsForValue().set(key, String.valueOf(status.getCode()), expireSeconds, TimeUnit.SECONDS); + log.debug("设置模板渲染状态(带过期): faceId={}, templateId={}, status={}, expireSeconds={}", + faceId, templateId, status.getDescription(), expireSeconds); + } + + /** + * 获取人脸模板渲染状态 + * @param faceId 人脸ID + * @param templateId 模板ID + * @return 渲染状态,缓存不存在时返回 null + */ + public TemplateRenderStatus getTemplateRenderStatus(Long faceId, Long templateId) { + if (faceId == null || templateId == null) { + log.warn("获取模板渲染状态参数为空: faceId={}, templateId={}", faceId, templateId); + return null; + } + String key = String.format(FACE_TEMPLATE_RENDER_KEY, faceId, templateId); + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + log.debug("模板渲染状态缓存不存在: faceId={}, templateId={}", faceId, templateId); + return null; + } + try { + int code = Integer.parseInt(value); + return TemplateRenderStatus.fromCode(code); + } catch (Exception e) { + log.error("模板渲染状态值解析失败: faceId={}, templateId={}, value={}", faceId, templateId, value, e); + return null; + } + } + + /** + * 判断模板是否已渲染完成 + * @param faceId 人脸ID + * @param templateId 模板ID + * @return true=已渲染,false=未渲染或正在渲染 + */ + public boolean isTemplateRendered(Long faceId, Long templateId) { + TemplateRenderStatus status = getTemplateRenderStatus(faceId, templateId); + return status != null && status.isRendered(); + } + + /** + * 判断模板是否正在渲染 + * @param faceId 人脸ID + * @param templateId 模板ID + * @return true=正在渲染,false=未渲染或已完成 + */ + public boolean isTemplateRendering(Long faceId, Long templateId) { + TemplateRenderStatus status = getTemplateRenderStatus(faceId, templateId); + return status != null && status.isRendering(); + } + + /** + * 删除人脸模板渲染状态缓存 + * @param faceId 人脸ID + * @param templateId 模板ID + */ + public void deleteTemplateRenderStatus(Long faceId, Long templateId) { + if (faceId == null || templateId == null) { + return; + } + String key = String.format(FACE_TEMPLATE_RENDER_KEY, faceId, templateId); + redisTemplate.delete(key); + log.debug("删除模板渲染状态缓存: faceId={}, templateId={}", faceId, templateId); + } + + /** + * 删除人脸的所有模板渲染状态(使用模式匹配) + * 注意:此操作可能影响性能,谨慎使用 + * @param faceId 人脸ID + */ + public void deleteAllTemplateRenderStatus(Long faceId) { + if (faceId == null) { + return; + } + String pattern = String.format(FACE_TEMPLATE_RENDER_KEY, faceId, "*"); + var keys = redisTemplate.keys(pattern); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.debug("批量删除模板渲染状态缓存: faceId={}, count={}", faceId, keys.size()); + } + } +} diff --git a/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java b/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java deleted file mode 100644 index 9abd7e0e..00000000 --- a/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.ycwl.basic.biz; - -import com.ycwl.basic.mapper.FaceMapper; -import com.ycwl.basic.mapper.TaskMapper; -import com.ycwl.basic.mapper.VideoMapper; -import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO; -import com.ycwl.basic.model.pc.face.entity.FaceEntity; -import com.ycwl.basic.model.pc.face.resp.FaceRespVO; -import com.ycwl.basic.model.pc.task.req.TaskReqQuery; -import com.ycwl.basic.model.pc.task.resp.TaskRespVO; -import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; -import com.ycwl.basic.model.pc.video.entity.VideoEntity; -import com.ycwl.basic.repository.FaceRepository; -import com.ycwl.basic.repository.TemplateRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -@Component -public class TaskStatusBiz { - public static final String TASK_STATUS_USER_CACHE_KEY = "task:status:user:%s:face:%s"; - public static final String TASK_STATUS_FACE_CACHE_KEY = "task:status:face:%s"; - public static final String TASK_STATUS_FACE_CACHE_KEY_CUT = "task:status:face:%s:cut"; - public static final String TASK_STATUS_FACE_CACHE_KEY_TEMPLATE = "task:status:face:%s:tpl:%s"; - - @Autowired - private RedisTemplate redisTemplate; - @Autowired - private FaceRepository faceRepository; - @Autowired - private TemplateRepository templateRepository; - @Autowired - private FaceMapper faceMapper; - @Autowired - private TaskMapper taskMapper; - @Autowired - private VideoMapper videoMapper; - @Autowired - private TemplateBiz templateBiz; - - public boolean getUserHaveFace(Long userId, Long faceId) { - if (userId == null || faceId == null) { - return false; - } - if (redisTemplate.hasKey(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId))) { - return true; - } - FaceEntity face = faceRepository.getFace(faceId); - if (face == null) { - return false; - } - if (face.getMemberId().equals(userId)) { - redisTemplate.opsForValue().set(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId), "1", 3600, TimeUnit.SECONDS); - return true; - } else { - return false; - } - } - - public void setFaceCutStatus(Long faceId, int status) { - redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId), String.valueOf(status), 3600, TimeUnit.SECONDS); - } - - public void setFaceTemplateStatus(Long faceId, Long templateId, Long videoId) { - redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId), String.valueOf(videoId), 3600, TimeUnit.SECONDS); - } - - public VideoTaskStatusVO getScenicUserStatus(Long scenicId, Long userId) { - FaceRespVO lastFace = faceMapper.findLastFaceByScenicAndUserId(scenicId, userId); - VideoTaskStatusVO response = new VideoTaskStatusVO(); - if (lastFace == null) { - response.setStatus(-1); - return response; - } - return getFaceStatus(lastFace.getId()); - } - - public VideoTaskStatusVO getFaceStatus(Long faceId) { - FaceEntity face = faceRepository.getFace(faceId); - VideoTaskStatusVO response = new VideoTaskStatusVO(); - if (face == null) { - response.setStatus(-1); - return response; - } - response.setScenicId(face.getScenicId()); - response.setFaceId(faceId); - List templateList = templateRepository.getTemplateListByScenicId(face.getScenicId()); - response.setMaxCount(templateList.size()); - int alreadyFinished = 0; - for (TemplateRespVO template : templateList) { - response.setTemplateId(template.getId()); - long videoId = getFaceTemplateVideoId(faceId, template.getId()); - if (videoId <= 0) { - response.setStatus(2); - } else { - response.setVideoId(videoId); - alreadyFinished++; - } - } - response.setCount(alreadyFinished); - if (alreadyFinished == 0) { - response.setStatus(0); - } else { - response.setStatus(1); - } - if (alreadyFinished == 0) { - int faceCutStatus = getFaceCutStatus(faceId); - if (faceCutStatus != 1) { - // 正在切片 - if (templateBiz.determineTemplateCanGenerate(templateList.getFirst().getId(), faceId, false)) { - response.setStatus(2); - } else { - response.setStatus(0); - } - } - } - return response; - } - - public VideoTaskStatusVO getFaceTemplateStatus(Long faceId, Long templateId) { - FaceEntity face = faceRepository.getFace(faceId); - VideoTaskStatusVO response = new VideoTaskStatusVO(); - if (face == null) { - response.setStatus(-1); - return response; - } - response.setScenicId(face.getScenicId()); - response.setFaceId(faceId); - response.setTemplateId(templateId); - long videoId = getFaceTemplateVideoId(faceId, templateId); - if (videoId < 0) { - int faceCutStatus = getFaceCutStatus(faceId); - if (faceCutStatus != 1) { - // 正在切片 - response.setStatus(2); - return response; - } - } else if (videoId == 0) { - response.setStatus(2); - } else { - response.setVideoId(videoId); - response.setStatus(1); - } - return response; - } - - public int getFaceCutStatus(Long faceId) { - if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId))) { - String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId)); - if (status != null) { - return Integer.parseInt(status); - } - } - return 1; - } - - public long getFaceTemplateVideoId(Long faceId, Long templateId) { - if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId))) { - String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId)); - if (status != null) { - return Long.parseLong(status); - } - } - TaskReqQuery taskReqQuery = new TaskReqQuery(); - taskReqQuery.setFaceId(faceId); - taskReqQuery.setTemplateId(templateId); - List list = taskMapper.list(taskReqQuery); - Optional min = list.stream().min(Comparator.comparing(TaskRespVO::getCreateTime)); - if (min.isPresent()) { - TaskRespVO task = min.get(); - long taskStatus = 0; - if (task.getStatus() == 1) { - // 已完成 - VideoEntity video = videoMapper.findByTaskId(task.getId()); - if (video != null) { - taskStatus = video.getId(); - } - } - setFaceTemplateStatus(faceId, templateId, taskStatus); - } else { - // 从来没生成过 - setFaceTemplateStatus(faceId, templateId, -1L); - return -1; - } - return 0; - } -} diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java index d1378a7a..1f563d22 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java @@ -104,7 +104,6 @@ public class AppGoodsController { // 查询用户当前景区的具体模版视频合成任务状态 1 合成中 2 合成成功 @GetMapping("/task/face/{faceId}/template/{templateId}") public ApiResponse getTemplateTaskStatus(@PathVariable("faceId") Long faceId, @PathVariable("templateId") Long templateId) { - JwtInfo worker = JwtTokenUtil.getWorker(); return ApiResponse.success(goodsService.getTaskStatusByTemplateId(faceId, templateId)); } diff --git a/src/main/java/com/ycwl/basic/enums/FaceCutStatus.java b/src/main/java/com/ycwl/basic/enums/FaceCutStatus.java new file mode 100644 index 00000000..16b7c409 --- /dev/null +++ b/src/main/java/com/ycwl/basic/enums/FaceCutStatus.java @@ -0,0 +1,65 @@ +package com.ycwl.basic.enums; + +/** + * 人脸视频切片状态枚举 + */ +public enum FaceCutStatus { + + /** + * 正在切片中 + */ + CUTTING(0, "正在切片中"), + + /** + * 切片已完成 + */ + COMPLETED(1, "切片已完成"), + + /** + * 等待用户选择模板 + */ + WAITING_USER_SELECT(2, "等待用户选择模板"); + + private final int code; + private final String description; + + FaceCutStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static FaceCutStatus fromCode(int code) { + for (FaceCutStatus status : values()) { + if (status.code == code) { + return status; + } + } + throw new IllegalArgumentException("Unknown FaceCutStatus code: " + code); + } + + /** + * 根据code获取枚举,如果不存在则返回默认值 + * @param code 状态码 + * @param defaultStatus 默认状态 + * @return 枚举值 + */ + public static FaceCutStatus fromCodeOrDefault(int code, FaceCutStatus defaultStatus) { + for (FaceCutStatus status : values()) { + if (status.code == code) { + return status; + } + } + return defaultStatus; + } +} diff --git a/src/main/java/com/ycwl/basic/enums/FacePieceUpdateStatus.java b/src/main/java/com/ycwl/basic/enums/FacePieceUpdateStatus.java new file mode 100644 index 00000000..37303929 --- /dev/null +++ b/src/main/java/com/ycwl/basic/enums/FacePieceUpdateStatus.java @@ -0,0 +1,64 @@ +package com.ycwl.basic.enums; + +/** + * 人脸片段更新状态枚举 + * 用于标记人脸对应的视频片段是否有新增更新 + */ +public enum FacePieceUpdateStatus { + + /** + * 有新片段 + * Redis键不存在时的默认状态,代表有新的视频片段产生 + */ + HAS_NEW_PIECES(0, "有新片段"), + + /** + * 无新片段 + * Redis键存在时的状态,代表当前没有新的视频片段 + */ + NO_NEW_PIECES(1, "无新片段"); + + private final int code; + private final String description; + + FacePieceUpdateStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static FacePieceUpdateStatus fromCode(int code) { + for (FacePieceUpdateStatus status : values()) { + if (status.code == code) { + return status; + } + } + throw new IllegalArgumentException("Unknown FacePieceUpdateStatus code: " + code); + } + + /** + * 根据Redis键是否存在判断状态 + * @param keyExists Redis键是否存在 + * @return 键存在返回NO_NEW_PIECES,键不存在返回HAS_NEW_PIECES + */ + public static FacePieceUpdateStatus fromKeyExists(boolean keyExists) { + return keyExists ? NO_NEW_PIECES : HAS_NEW_PIECES; + } + + /** + * 判断是否有新片段 + */ + public boolean hasNewPieces() { + return this == HAS_NEW_PIECES; + } +} diff --git a/src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java b/src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java new file mode 100644 index 00000000..793a13d8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/enums/TemplateRenderStatus.java @@ -0,0 +1,75 @@ +package com.ycwl.basic.enums; + +/** + * 人脸对应模板渲染状态枚举 + */ +public enum TemplateRenderStatus { + + NONE(0, "没有渲染"), + /** + * 正在渲染中 + */ + RENDERING(1, "正在渲染中"), + + /** + * 已渲染完成 + */ + RENDERED(2, "已渲染完成"); + + private final int code; + private final String description; + + TemplateRenderStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static TemplateRenderStatus fromCode(int code) { + for (TemplateRenderStatus status : values()) { + if (status.code == code) { + return status; + } + } + throw new IllegalArgumentException("Unknown TemplateRenderStatus code: " + code); + } + + /** + * 根据code获取枚举,如果不存在则返回默认值 + * @param code 状态码 + * @param defaultStatus 默认状态 + * @return 枚举值 + */ + public static TemplateRenderStatus fromCodeOrDefault(int code, TemplateRenderStatus defaultStatus) { + for (TemplateRenderStatus status : values()) { + if (status.code == code) { + return status; + } + } + return defaultStatus; + } + + /** + * 判断是否已完成渲染 + */ + public boolean isRendered() { + return this == RENDERED; + } + + /** + * 判断是否正在渲染 + */ + public boolean isRendering() { + return this == RENDERING; + } +} diff --git a/src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java b/src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java new file mode 100644 index 00000000..b5e6f330 --- /dev/null +++ b/src/main/java/com/ycwl/basic/enums/VideoTaskStatus.java @@ -0,0 +1,159 @@ +package com.ycwl.basic.enums; + +/** + * 视频任务状态枚举 + * 用于前端展示任务状态 + */ +public enum VideoTaskStatus { + + /** + * 无效人脸(景区级别) + */ + INVALID_FACE_SCENIC(-2, "尚未录入有效人脸"), + + /** + * 无效人脸(人脸级别) + */ + INVALID_FACE(-1, "尚未录入有效人脸"), + + /** + * 待制作 + * 人脸已录入,但尚未开始合成视频 + */ + PENDING(0, "专属视频待制作"), + + /** + * 合成成功 + * 已为用户合成视频 + */ + SUCCESS(1, "AI已为您合成视频"), + + /** + * 合成中 + * 正在合成专属视频 + */ + PROCESSING(2, "专属视频合成中"), + + /** + * 合成失败 + */ + FAILED(3, "视频合成失败"), + + /** + * 切片中 + * 正在检索新的视频片段 + */ + CUTTING(4, "正在检索新的视频片段"); + + private final int code; + private final String description; + + VideoTaskStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static VideoTaskStatus fromCode(int code) { + for (VideoTaskStatus status : values()) { + if (status.code == code) { + return status; + } + } + throw new IllegalArgumentException("Unknown VideoTaskStatus code: " + code); + } + + /** + * 根据code获取枚举,如果不存在则返回默认值 + * @param code 状态码 + * @param defaultStatus 默认状态 + * @return 枚举值 + */ + public static VideoTaskStatus fromCodeOrDefault(int code, VideoTaskStatus defaultStatus) { + for (VideoTaskStatus status : values()) { + if (status.code == code) { + return status; + } + } + return defaultStatus; + } + + /** + * 获取前端展示文案 + * @param count 已合成视频数量(仅SUCCESS状态使用) + * @return 展示文案 + */ + public String getDisplayText(long count) { + if (this == SUCCESS && count > 0) { + return "AI已为您合成" + count + "个视频"; + } + return description; + } + + /** + * 根据业务逻辑判断最终展示状态 + * @param taskStatus 任务状态码 + * @param cutStatus 切片状态码(来自FaceCutStatus) + * @param count 已合成视频数量 + * @return 最终展示的状态 + */ + public static VideoTaskStatus resolveDisplayStatus(int taskStatus, int cutStatus, long count) { + VideoTaskStatus status = fromCodeOrDefault(taskStatus, PENDING); + + // 优先级1: 无效人脸状态 + if (status == INVALID_FACE_SCENIC || status == INVALID_FACE) { + return status; + } + + // 优先级2: 切片状态优先(当任务状态为待制作且切片状态为正在切片时) + if (status == PENDING && cutStatus == 0) { + return CUTTING; + } + + // 优先级3: 返回任务状态 + return status; + } + + /** + * 获取最终展示文案 + * @param taskStatus 任务状态码 + * @param cutStatus 切片状态码 + * @param count 已合成视频数量 + * @return 展示文案 + */ + public static String getDisplayText(int taskStatus, int cutStatus, long count) { + VideoTaskStatus status = resolveDisplayStatus(taskStatus, cutStatus, count); + return status.getDisplayText(count); + } + + /** + * 判断是否为成功状态 + */ + public boolean isSuccess() { + return this == SUCCESS; + } + + /** + * 判断是否为处理中状态 + */ + public boolean isProcessing() { + return this == PROCESSING || this == CUTTING; + } + + /** + * 判断是否为失败状态 + */ + public boolean isFailed() { + return this == FAILED || this == INVALID_FACE || this == INVALID_FACE_SCENIC; + } +} diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStage.java index e15385d6..227faf1f 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStage.java @@ -1,6 +1,7 @@ package com.ycwl.basic.face.pipeline.stages; -import com.ycwl.basic.biz.TaskStatusBiz; +import com.ycwl.basic.biz.FaceStatusManager; +import com.ycwl.basic.enums.FaceCutStatus; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.pipeline.annotation.StageConfig; import com.ycwl.basic.pipeline.core.AbstractPipelineStage; @@ -37,7 +38,7 @@ public class CreateTaskStage extends AbstractPipelineStage private TaskService taskService; @Autowired - private TaskStatusBiz taskStatusBiz; + private FaceStatusManager faceStatusManager; @Override public String getName() { @@ -59,7 +60,7 @@ public class CreateTaskStage extends AbstractPipelineStage return StageResult.success("自动创建任务成功"); } else { // 配置为等待用户选择 - taskStatusBiz.setFaceCutStatus(faceId, 2); + faceStatusManager.setFaceCutStatus(faceId, FaceCutStatus.WAITING_USER_SELECT); log.debug("景区配置 face_select_first=true,跳过自动创建任务: faceId={}", faceId); return StageResult.skipped("等待用户手动选择"); } diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStage.java index cd08b236..c84b489b 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStage.java @@ -1,6 +1,7 @@ package com.ycwl.basic.face.pipeline.stages; -import com.ycwl.basic.biz.TaskStatusBiz; +import com.ycwl.basic.biz.FaceStatusManager; +import com.ycwl.basic.enums.FaceCutStatus; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.pipeline.annotation.StageConfig; import com.ycwl.basic.pipeline.core.AbstractPipelineStage; @@ -28,7 +29,7 @@ import org.springframework.stereotype.Component; public class SetTaskStatusStage extends AbstractPipelineStage { @Autowired - private TaskStatusBiz taskStatusBiz; + private FaceStatusManager faceStatusManager; @Override public String getName() { @@ -56,7 +57,7 @@ public class SetTaskStatusStage extends AbstractPipelineStage sourceGoodsList(GoodsReqQuery query) { @@ -258,83 +262,113 @@ public class GoodsServiceImpl implements GoodsService { } /** - * 查询用户当前景区的视频合成任务状态 + * 查询用户人脸对应的视频合成任务状态 * - * @param faceId 景区id - * @return 0没有任务 1 合成中 2 合成成功 + *

业务流程: + *

    + *
  1. 验证人脸是否存在
  2. + *
  3. 检查视频切片状态(CUTTING → 返回处理中)
  4. + *
  5. 遍历景区模板,检查渲染状态(RENDERING → 返回处理中)
  6. + *
  7. 统计已渲染完成的模板数量
  8. + *
  9. 根据切片状态返回最终结果: + *
      + *
    • WAITING_USER_SELECT → 处理中(等待用户选择模板)
    • + *
    • COMPLETED → 成功(查询已完成的视频信息)
    • + *
    + *
  10. + *
+ * + * @param faceId 人脸ID + * @return 视频任务状态VO,包含状态码、已完成数量、视频信息等 + *
    + *
  • status = -1: 人脸不存在
  • + *
  • status = 2: 处理中(切片中/渲染中/等待选择)
  • + *
  • status = 1: 成功(已有完成的视频)
  • + *
*/ @Override public VideoTaskStatusVO getTaskStatusByFaceId(Long faceId) { VideoTaskStatusVO response = new VideoTaskStatusVO(); + + // ==================== 第一步:验证人脸是否存在 ==================== FaceEntity face = faceRepository.getFace(faceId); if (face == null) { - response.setStatus(-1); + // 人脸不存在,返回无效人脸状态 + response.setStatus(VideoTaskStatus.INVALID_FACE.getCode()); return response; } - Long userId = face.getMemberId(); + + // 设置基本信息 response.setFaceId(faceId); response.setScenicId(face.getScenicId()); - int faceCutStatus = taskStatusBiz.getFaceCutStatus(faceId); - response.setCutStatus(faceCutStatus); - if (faceCutStatus == 0) { - // 切视频中,也显示正在处理 - response.setStatus(2); - return response; - } - List taskList = videoMapper.listRelationByFace(faceId); - if (faceCutStatus != 1 && taskList.isEmpty()) { - // 视频切成了能够获取视频的状态,但是没有任务,还是显示正在处理 - response.setStatus(0); - return response; - } - if (taskList.isEmpty()) { - response.setStatus(0); + + // ==================== 第二步:检查视频切片状态 ==================== + // 从缓存中获取切片状态(CUTTING/COMPLETED/WAITING_USER_SELECT) + FaceCutStatus status = faceStatusManager.getFaceCutStatus(faceId); + response.setCutStatus(status.getCode()); + + if (status == FaceCutStatus.CUTTING) { + // 视频片段正在切割中,用户需要等待切片完成 + // 前端展示:「正在检索新的视频片段」 + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); return response; } + + // ==================== 第三步:检查模板渲染状态 ==================== + // 获取该景区的所有视频模板 List templateList = templateRepository.getTemplateListByScenicId(response.getScenicId()); - List templateIds = templateList.stream().map(TemplateRespVO::getId).collect(Collectors.toList()); + AtomicInteger count = new AtomicInteger(); + + // 遍历所有模板,检查每个模板的渲染状态 + for (TemplateRespVO template : templateList) { + // 从缓存中获取该人脸在该模板下的渲染状态 + TemplateRenderStatus renderStatus = faceStatusManager.getTemplateRenderStatus(faceId, template.getId()); + + if (renderStatus == TemplateRenderStatus.RENDERING) { + // 发现有模板正在渲染中,立即返回处理中状态 + // 前端展示:「专属视频合成中」 + response.setTemplateId(template.getId()); + response.setCount(0); + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); + return response; + } + + if (renderStatus == TemplateRenderStatus.RENDERED) { + // 统计已渲染完成的模板数量 + count.incrementAndGet(); + } + } + + // 设置已完成数量和总模板数 + response.setCount(count.get()); response.setMaxCount(templateList.size()); - List notFinishedTasks = taskList.stream() - .filter(task -> templateIds.contains(task.getTemplateId())) - .filter(task -> { - TaskEntity taskById = videoTaskRepository.getTaskById(task.getTaskId()); - if (taskById == null) { - return true; - } - return taskById.getStatus() == 0 || taskById.getStatus() == 2; - }) - .collect(Collectors.toList()); - long finishedTask = taskList.stream() - .filter(task -> { - TaskEntity taskById = videoTaskRepository.getTaskById(task.getTaskId()); - if (taskById == null) { - return false; - } - return taskById.getStatus() == 1; - }) - .map(MemberVideoEntity::getTemplateId) - .distinct() - .count(); - response.setCount(finishedTask); - if (!notFinishedTasks.isEmpty()) { - response.setTemplateId(notFinishedTasks.getFirst().getTemplateId()); - response.setTaskId(notFinishedTasks.getFirst().getTaskId()); - response.setStatus(2); + + // ==================== 第四步:根据切片完成状态返回结果 ==================== + + if (status == FaceCutStatus.WAITING_USER_SELECT) { + // 切片已完成,但景区配置了 face_select_first=true + // 需要等待用户手动选择模板后才开始渲染 + // 前端展示:「专属视频合成中」 + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); return response; } - // 重查一下 - taskList = videoMapper.listRelationByFace(faceId); - MemberVideoEntity lastVideo = taskList.getLast(); - if (null == lastVideo.getVideoId()) { - response.setTemplateId(lastVideo.getTemplateId()); - response.setTaskId(lastVideo.getTaskId()); - response.setStatus(2); + + if (status == FaceCutStatus.COMPLETED) { + // 切片已完成,查询该人脸关联的视频信息 + List taskList = videoMapper.listRelationByFace(faceId); + + // 设置最新的视频信息(取最后一个) + response.setTaskId(taskList.getLast().getTaskId()); + response.setTemplateId(taskList.getLast().getTemplateId()); + response.setVideoId(taskList.getLast().getVideoId()); + + // 返回成功状态 + // 前端展示:「AI已为您合成{count}个视频」 + response.setStatus(VideoTaskStatus.SUCCESS.getCode()); return response; } - response.setTaskId(lastVideo.getTaskId()); - response.setTemplateId(lastVideo.getTemplateId()); - response.setVideoId(lastVideo.getVideoId()); - response.setStatus(1); + + // 兜底返回(理论上不应该走到这里) return response; } @@ -344,72 +378,109 @@ public class GoodsServiceImpl implements GoodsService { return getTaskStatusByFaceId(lastFaceByUserId.getId()); } + /** + * 查询指定人脸和模板的视频合成任务状态 + * + *

业务流程: + *

    + *
  1. 验证人脸是否存在
  2. + *
  3. 检查视频切片状态(CUTTING → 返回处理中)
  4. + *
  5. 检查指定模板的渲染状态(RENDERING → 返回处理中)
  6. + *
  7. 检查是否等待用户选择(WAITING_USER_SELECT → 返回处理中)
  8. + *
  9. 查询该模板对应的视频信息并返回成功状态
  10. + *
+ * + * @param faceId 人脸ID + * @param templateId 模板ID + * @return 视频任务状态VO,包含该模板的状态信息 + *
    + *
  • status = -1: 人脸不存在
  • + *
  • status = 0: 待制作(无任务记录)
  • + *
  • status = 2: 处理中(切片中/渲染中/等待选择)
  • + *
  • status = 1: 成功(该模板视频已完成)
  • + *
+ */ @Override public VideoTaskStatusVO getTaskStatusByTemplateId(Long faceId, Long templateId) { - List taskList = videoMapper.listRelationByFaceAndTemplate(faceId, templateId); VideoTaskStatusVO response = new VideoTaskStatusVO(); response.setFaceId(faceId); response.setTemplateId(templateId); - if (taskList.isEmpty()) { - response.setStatus(0); + + // ==================== 第一步:验证人脸是否存在 ==================== + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + // 人脸不存在,返回无效人脸状态 + response.setStatus(VideoTaskStatus.INVALID_FACE.getCode()); return response; } - response.setScenicId(taskList.getFirst().getScenicId()); - response.setMaxCount(templateRepository.getTemplateListByScenicId(response.getScenicId()).size()); - List notFinishedTasks = taskList.stream() - .filter(task -> { - TaskEntity taskById = videoTaskRepository.getTaskById(task.getTaskId()); - if (taskById == null) { - return true; - } - return taskById.getStatus() == 0 || taskById.getStatus() == 2; - }).collect(Collectors.toList()); - long finishedTask = taskList.stream() - .filter(task -> { - TaskEntity taskById = videoTaskRepository.getTaskById(task.getTaskId()); - if (taskById == null) { - return false; - } - return taskById.getStatus() == 1; - }).count(); - response.setCount(finishedTask); - int faceCutStatus = taskStatusBiz.getFaceCutStatus(faceId); - if (Integer.valueOf(0).equals(faceCutStatus)) { - if (!notFinishedTasks.isEmpty()) { - response.setTemplateId(notFinishedTasks.getFirst().getTemplateId()); + + // 设置景区信息 + response.setScenicId(face.getScenicId()); + + // ==================== 第二步:检查视频切片状态 ==================== + // 从缓存中获取切片状态 + FaceCutStatus cutStatus = faceStatusManager.getFaceCutStatus(faceId); + response.setCutStatus(cutStatus.getCode()); + + if (cutStatus == FaceCutStatus.CUTTING) { + // 视频片段正在切割中,无法生成任何视频 + // 前端展示:「正在检索新的视频片段」 + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); + return response; + } + + // ==================== 第三步:检查指定模板的渲染状态 ==================== + // 从缓存中获取该人脸在该模板下的渲染状态 + TemplateRenderStatus renderStatus = faceStatusManager.getTemplateRenderStatus(faceId, templateId); + + if (renderStatus == TemplateRenderStatus.RENDERING) { + // 该模板正在渲染中 + // 前端展示:「专属视频合成中」 + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); + return response; + } + + if (renderStatus == TemplateRenderStatus.NONE) { + // 该模板从未开始渲染 + // 可能是用户还没选择该模板,或系统还未开始处理 + response.setStatus(VideoTaskStatus.PENDING.getCode()); + return response; + } + + // ==================== 第四步:检查是否等待用户选择 ==================== + if (cutStatus == FaceCutStatus.WAITING_USER_SELECT && renderStatus != TemplateRenderStatus.RENDERED) { + // 切片已完成,但需要等待用户手动选择模板 + // 前端展示:「专属视频合成中」 + response.setStatus(VideoTaskStatus.PROCESSING.getCode()); + return response; + } + + // ==================== 第五步:查询已完成的视频信息 ==================== + if (renderStatus == TemplateRenderStatus.RENDERED) { + // 该模板已渲染完成,查询对应的视频信息 + List taskList = videoMapper.listRelationByFaceAndTemplate(faceId, templateId); + + if (taskList.isEmpty()) { + // 理论上不应该出现:渲染完成但无视频记录 + // 可能是数据不一致,返回待制作状态 + response.setStatus(VideoTaskStatus.PENDING.getCode()); + return response; } - response.setStatus(2); - return response; - } - if (!notFinishedTasks.isEmpty()) { - response.setTemplateId(notFinishedTasks.getFirst().getTemplateId()); - response.setTaskId(notFinishedTasks.getFirst().getTaskId()); - response.setStatus(2); - return response; - } - MemberVideoEntity lastVideo = taskList.getLast(); - response.setTaskId(lastVideo.getTaskId()); - response.setTemplateId(lastVideo.getTemplateId()); - response.setVideoId(lastVideo.getVideoId()); - if (null != lastVideo.getVideoId()) { - response.setStatus(1); + + // 获取最新的视频记录(取最后一个) + MemberVideoEntity lastVideo = taskList.getLast(); + response.setTaskId(lastVideo.getTaskId()); response.setVideoId(lastVideo.getVideoId()); - } else { - TaskEntity taskById = videoTaskRepository.getTaskById(lastVideo.getTaskId()); - if (taskById == null) { - response.setStatus(1); - } else { - videoTaskRepository.clearTaskCache(lastVideo.getTaskId()); - if (taskById.getStatus() == 1) { - response.setStatus(1); - response.setVideoId(lastVideo.getVideoId()); - } else if (taskById.getStatus() == 0 || taskById.getStatus() == 2) { - response.setStatus(2); - } else { - response.setStatus(1); - } - } + response.setCount(1); // 该模板已完成1个视频 + + // 返回成功状态 + // 前端展示:「AI已为您合成视频」 + response.setStatus(VideoTaskStatus.SUCCESS.getCode()); + return response; } + + // 兜底:默认返回待制作状态 + response.setStatus(VideoTaskStatus.PENDING.getCode()); return response; } @@ -718,6 +789,13 @@ public class GoodsServiceImpl implements GoodsService { return result; } + FacePieceUpdateStatus updateStatus = faceStatusManager.getFacePieceUpdateStatus(video.getFaceId(), video.getTemplateId()); + if (updateStatus == FacePieceUpdateStatus.NO_NEW_PIECES) { + log.info("无新片段: faceId={}, templateId={}", video.getFaceId(), video.getTemplateId()); + result.setCanUpdate(false); + return result; + } + Long taskId = video.getTaskId(); if (taskId == null) { log.error("视频没有关联任务: videoId={}", videoId); @@ -730,6 +808,9 @@ public class GoodsServiceImpl implements GoodsService { result.setTemplateId(video.getTemplateId()); TaskUpdateResult taskResult = videoTaskRepository.checkTaskUpdate(taskId); + if (!taskResult.isCanUpdate()) { + faceStatusManager.markNoNewPieces(video.getFaceId(), video.getTemplateId()); + } result.setCanUpdate(taskResult.isCanUpdate()); result.setNewSegmentCount(taskResult.getNewSegmentCount()); result.setTotalSegmentCount(taskResult.getTotalSegmentCount()); diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java index 712561d5..04f61c31 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java @@ -466,7 +466,7 @@ public class FaceServiceImpl implements FaceService { contentPageVO.setLockType(0); } else if (taskById.getStatus() == 3) { contentPageVO.setLockType(2); - } else { + } else if (taskById.getStatus() == 0 || taskById.getStatus() == 2) { contentPageVO.setLockType(-9); // 正在生成 } contentPageVO.setContentType(0); diff --git a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java index 012a48e4..35e990ed 100644 --- a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java +++ b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java @@ -1,19 +1,17 @@ package com.ycwl.basic.service.pc.orchestrator; import cn.hutool.core.date.DateUtil; -import com.ycwl.basic.biz.TaskStatusBiz; +import com.ycwl.basic.biz.FaceStatusManager; +import com.ycwl.basic.enums.FaceCutStatus; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest; import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO; -import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity; -import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper; import com.ycwl.basic.puzzle.service.IPuzzleGenerateService; import com.ycwl.basic.puzzle.service.IPuzzleTemplateService; import java.util.HashMap; import java.util.Map; -import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.integration.common.manager.ScenicConfigManager; @@ -99,9 +97,9 @@ public class FaceMatchingOrchestrator { @Autowired private IPuzzleGenerateService puzzleGenerateService; @Autowired - private TaskStatusBiz taskStatusBiz; - @Autowired private RedisTemplate redisTemplate; + @Autowired + private FaceStatusManager faceStatusManager; /** * 编排人脸匹配的完整流程 @@ -117,7 +115,7 @@ public class FaceMatchingOrchestrator { if (isNew) { // 新用户,设置任务状态为待处理 - taskStatusBiz.setFaceCutStatus(faceId, 0); + faceStatusManager.setFaceCutStatus(faceId, FaceCutStatus.CUTTING); } // 步骤1: 数据准备 @@ -349,7 +347,7 @@ public class FaceMatchingOrchestrator { taskService.autoCreateTaskByFaceId(faceId); } else { log.debug("景区配置 face_select_first=true,跳过自动创建任务:faceId={}", faceId); - taskStatusBiz.setFaceCutStatus(faceId, 2); + faceStatusManager.setFaceCutStatus(faceId, FaceCutStatus.WAITING_USER_SELECT); } } diff --git a/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java b/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java index 6c97450b..9acd70fe 100644 --- a/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java @@ -2,6 +2,9 @@ package com.ycwl.basic.service.task.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.crypto.digest.MD5; +import com.ycwl.basic.biz.FaceStatusManager; +import com.ycwl.basic.enums.FaceCutStatus; +import com.ycwl.basic.enums.TemplateRenderStatus; import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.RenderWorkerConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager; @@ -11,7 +14,6 @@ import com.ycwl.basic.repository.MemberRelationRepository; import com.ycwl.basic.repository.SourceRepository; import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.biz.OrderBiz; -import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.biz.TemplateBiz; import com.ycwl.basic.constant.StorageConstant; import com.ycwl.basic.mapper.FaceMapper; @@ -116,8 +118,6 @@ public class TaskTaskServiceImpl implements TaskService { private final ReentrantLock lock = new ReentrantLock(); @Autowired - private TaskStatusBiz taskStatusBiz; - @Autowired private DeviceRepository deviceRepository; @Autowired private VideoReUploader videoReUploader; @@ -131,6 +131,8 @@ public class TaskTaskServiceImpl implements TaskService { private ZtMessageProducerService ztMessageProducerService; @Autowired private NotificationAuthUtils notificationAuthUtils; + @Autowired + private FaceStatusManager faceStatusManager; private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) { String accessKey = req.getAccessKey(); @@ -295,7 +297,6 @@ public class TaskTaskServiceImpl implements TaskService { task.templateId = null; task.memberId = faceRespVO.getMemberId(); task.callback = () -> { - log.info("task callback: {}", task); }; VideoPieceGetter.addTask(task); return; @@ -340,9 +341,9 @@ public class TaskTaskServiceImpl implements TaskService { if (!forceCreate) { if (templateBiz.determineTemplateCanGenerate(templateId, faceId, false)) { // 临时写死,当自动生成视频,切片也算合成中,并更新状态 - taskStatusBiz.setFaceCutStatus(face.getId(), 0); + faceStatusManager.setFaceCutStatus(face.getId(), FaceCutStatus.CUTTING); } else { - taskStatusBiz.setFaceCutStatus(face.getId(), 2); + faceStatusManager.setFaceCutStatus(face.getId(), FaceCutStatus.WAITING_USER_SELECT); } } @@ -358,19 +359,18 @@ public class TaskTaskServiceImpl implements TaskService { }).toList() .stream().map(FaceSampleEntity::getId).collect(Collectors.toList()); log.info("视频切分任务: faceId={}, 原始数量={}, 筛选后数量={}", faceId, faceSampleList.size(), faceSampleIds.size()); - List sourceList = sourceMapper.listVideoByScenicFaceRelation(face.getScenicId(), faceId); VideoPieceGetter.Task task = new VideoPieceGetter.Task(); task.faceId = faceId; task.faceSampleIds = faceSampleIds; task.templateId = templateId; task.memberId = face.getMemberId(); task.callback = () -> { - log.info("task callback: {}", task); - + faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERING); // 只有在非强制创建时才进行模板生成判断 if (!forceCreate) { boolean canGenerate = templateBiz.determineTemplateCanGenerate(templateId, faceId); if (!canGenerate) { + faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE); log.info("task callback: 不能生成,templateId: {}", templateId); return; } @@ -378,11 +378,14 @@ public class TaskTaskServiceImpl implements TaskService { Map> allTaskParams = sourceRepository.getTaskParams(faceId, templateId); if (allTaskParams.isEmpty()) { + faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE); + log.info("task callback: 任务参数没有,templateId: {}", templateId); return; } Map> sourcesMap = templateBiz.filterTaskParams(templateId, allTaskParams); if (sourcesMap.isEmpty()) { + faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.NONE); log.info("task callback: 筛选后无有效源数据,templateId: {}", templateId); return; } @@ -415,7 +418,8 @@ public class TaskTaskServiceImpl implements TaskService { memberVideoEntity.setIsBuy(taskVideoRelation.getIsBuy()); memberVideoEntity.setOrderId(taskVideoRelation.getOrderId()); } - taskMapper.deleteById(taskEntity.getId()); + // TODO: FIXME 临时解决 + taskMapper.updateStatus(taskEntity.getId(), taskEntity.getStatus() + 10); } } if (taskEntity == null) { @@ -449,12 +453,8 @@ public class TaskTaskServiceImpl implements TaskService { } videoMapper.addRelation(memberVideoEntity); memberRelationRepository.clearVCacheByFace(faceId); - - // 只有在非强制创建时才更新切割任务状态 - if (!forceCreate) { - // 任务生成了,需要更新切割任务状态 - taskStatusBiz.setFaceCutStatus(faceId, 2); - } + faceStatusManager.setTemplateRenderStatus(faceId, templateId, TemplateRenderStatus.RENDERED); + faceStatusManager.markNoNewPieces(faceId, templateId); }; VideoPieceGetter.addTask(task); } diff --git a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java index 0ede072b..b7b9d3ee 100644 --- a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java +++ b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java @@ -1,12 +1,13 @@ package com.ycwl.basic.task; import cn.hutool.core.thread.ThreadFactoryBuilder; +import com.ycwl.basic.biz.FaceStatusManager; import com.ycwl.basic.biz.OrderBiz; -import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.constant.StorageConstant; import com.ycwl.basic.device.DeviceFactory; import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.operator.IDeviceStorageOperator; +import com.ycwl.basic.enums.FaceCutStatus; import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; @@ -74,8 +75,6 @@ public class VideoPieceGetter { @Autowired private TemplateRepository templateRepository; @Autowired - private TaskStatusBiz taskStatusBiz; - @Autowired private VideoReUploader videoReUploader; @Autowired private ScenicRepository scenicRepository; @@ -85,6 +84,8 @@ public class VideoPieceGetter { private MemberRelationRepository memberRelationRepository; public static final String PROBE_SIZE = "16M"; + @Autowired + private FaceStatusManager faceStatusManager; @Data public static class Task { @@ -151,19 +152,21 @@ public class VideoPieceGetter { ); Map currentUnFinPlaceholder = new ConcurrentHashMap<>(); List list = faceSampleMapper.listByIds(task.getFaceSampleIds()); - Map pairDeviceMap = new ConcurrentHashMap<>(); - if (!list.isEmpty()) { - Long scenicId = list.getFirst().getScenicId(); - List allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId); - allDeviceByScenicId.forEach(device -> { - Long deviceId = device.getId(); - DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId); - Long pairDevice = deviceConfig.getLong("pair_device"); - if (pairDevice != null) { - pairDeviceMap.putIfAbsent(deviceId, pairDevice); - } - }); + if (list == null || list.isEmpty()) { + task.callback.onInvoke(); + return; } + Map pairDeviceMap = new ConcurrentHashMap<>(); + Long scenicId = list.getFirst().getScenicId(); + List allDeviceByScenicId = deviceRepository.getAllDeviceByScenicId(scenicId); + allDeviceByScenicId.forEach(device -> { + Long deviceId = device.getId(); + DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(deviceId); + Long pairDevice = deviceConfig.getLong("pair_device"); + if (pairDevice != null) { + pairDeviceMap.putIfAbsent(deviceId, pairDevice); + } + }); Map> collection = list.stream() .filter(faceSample -> { if (templatePlaceholder != null) { @@ -253,12 +256,16 @@ public class VideoPieceGetter { invoke.set(true); log.info("[Callback调用] 所有placeholder已满足,currentUnFinPlaceholder为空,提前调用callback"); task.getCallback().onInvoke(); - } else { - log.warn("[Callback跳过] 所有placeholder已满足,但callback已被调用过"); } } } }); + if (task.faceId != null) { + // 经过切片后,可能有新的人脸切片生成,需要更新人脸状态 + templateRepository.getTemplateListByScenicId(scenicId).forEach(template -> { + faceStatusManager.markHasNewPieces(task.faceId, template.getId()); + }); + } }); }); try { @@ -271,18 +278,16 @@ public class VideoPieceGetter { } catch (InterruptedException e) { log.info("executor已中断![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount()); } finally { - if (task.faceId != null) { - taskStatusBiz.setFaceCutStatus(task.faceId, 1); + if (null != task.getCallback()) { + if (!invoke.get()) { + invoke.set(true); + log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}", + currentUnFinPlaceholder.size()); + task.getCallback().onInvoke(); + } } - } - if (null != task.getCallback()) { - if (!invoke.get()) { - invoke.set(true); - log.info("[Callback调用] 兜底调用callback,currentUnFinPlaceholder剩余设备数={}", - currentUnFinPlaceholder.size()); - task.getCallback().onInvoke(); - } else { - log.info("[Callback跳过] 兜底检查,callback已被调用过"); + if (task.faceId != null) { + faceStatusManager.setFaceCutStatus(task.faceId, FaceCutStatus.COMPLETED); } } if (task.getFaceId() != null) { diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java index 6fa58de0..3bebab8e 100644 --- a/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java @@ -1,6 +1,5 @@ package com.ycwl.basic.face.pipeline.stages; -import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.pipeline.core.StageResult; import com.ycwl.basic.model.pc.face.entity.FaceEntity; @@ -28,8 +27,6 @@ class CreateTaskStageTest { @Mock private TaskService taskService; - @Mock - private TaskStatusBiz taskStatusBiz; @InjectMocks private CreateTaskStage stage; @@ -63,7 +60,7 @@ class CreateTaskStageTest { assertTrue(result.getMessage().contains("自动创建任务成功")); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); verify(taskService, times(1)).autoCreateTaskByFaceId(1L); - verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); +// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); } @Test @@ -79,7 +76,7 @@ class CreateTaskStageTest { assertTrue(result.isSkipped()); assertTrue(result.getMessage().contains("等待用户手动选择")); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); - verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); +// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); verify(taskService, never()).autoCreateTaskByFaceId(anyLong()); } @@ -97,7 +94,7 @@ class CreateTaskStageTest { assertTrue(result.getMessage().contains("任务创建失败")); verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L); verify(taskService, never()).autoCreateTaskByFaceId(anyLong()); - verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); +// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); } @Test @@ -122,8 +119,8 @@ class CreateTaskStageTest { // Given: 设置状态失败 when(scenicConfigFacade.isFaceSelectFirst(10L)) .thenReturn(true); - doThrow(new RuntimeException("Status set error")) - .when(taskStatusBiz).setFaceCutStatus(1L, 2); +// doThrow(new RuntimeException("Status set error")) +// .when(taskStatusBiz).setFaceCutStatus(1L, 2); // When StageResult result = stage.execute(context); @@ -131,7 +128,7 @@ class CreateTaskStageTest { // Then assertTrue(result.isDegraded()); assertTrue(result.getMessage().contains("任务创建失败")); - verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); +// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2); } @Test diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStageTest.java index 2a1e4fa4..e30ec14a 100644 --- a/src/test/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStageTest.java +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/SetTaskStatusStageTest.java @@ -1,21 +1,16 @@ package com.ycwl.basic.face.pipeline.stages; -import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.face.pipeline.core.FaceMatchingContext; import com.ycwl.basic.pipeline.core.StageResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; /** * SetTaskStatusStage 单元测试 @@ -23,9 +18,6 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class SetTaskStatusStageTest { - @Mock - private TaskStatusBiz taskStatusBiz; - @InjectMocks private SetTaskStatusStage stage; @@ -41,7 +33,7 @@ class SetTaskStatusStageTest { // Given context = FaceMatchingContext.forAutoMatching(1L, true); - doNothing().when(taskStatusBiz).setFaceCutStatus(1L, 0); +// doNothing().when(taskStatusBiz).setFaceCutStatus(1L, 0); // When StageResult result = stage.execute(context); @@ -49,7 +41,7 @@ class SetTaskStatusStageTest { // Then assertTrue(result.isSuccess()); assertTrue(result.getMessage().contains("任务状态已设置")); - verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 0); +// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 0); } @Test @@ -63,7 +55,7 @@ class SetTaskStatusStageTest { // Then assertTrue(result.isSkipped()); assertTrue(result.getMessage().contains("非新用户")); - verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); +// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); } @Test @@ -71,8 +63,8 @@ class SetTaskStatusStageTest { // Given context = FaceMatchingContext.forAutoMatching(1L, true); - doThrow(new RuntimeException("Database error")) - .when(taskStatusBiz).setFaceCutStatus(1L, 0); +// doThrow(new RuntimeException("Database error")) +// .when(taskStatusBiz).setFaceCutStatus(1L, 0); // When StageResult result = stage.execute(context); @@ -80,7 +72,7 @@ class SetTaskStatusStageTest { // Then assertTrue(result.isDegraded()); // 降级处理,不影响主流程 assertTrue(result.getMessage().contains("任务状态设置失败")); - verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 0); +// verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 0); } @Test @@ -88,14 +80,14 @@ class SetTaskStatusStageTest { // Given: 不同的faceId context = FaceMatchingContext.forAutoMatching(999L, true); - doNothing().when(taskStatusBiz).setFaceCutStatus(999L, 0); +// doNothing().when(taskStatusBiz).setFaceCutStatus(999L, 0); // When StageResult result = stage.execute(context); // Then assertTrue(result.isSuccess()); - verify(taskStatusBiz, times(1)).setFaceCutStatus(999L, 0); +// verify(taskStatusBiz, times(1)).setFaceCutStatus(999L, 0); } @Test @@ -108,7 +100,7 @@ class SetTaskStatusStageTest { // Then assertTrue(result.isSkipped()); - verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); +// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); } @Test @@ -122,7 +114,7 @@ class SetTaskStatusStageTest { // Then assertTrue(result.isSkipped()); - verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); +// verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt()); } @Test @@ -130,8 +122,8 @@ class SetTaskStatusStageTest { // Given context = FaceMatchingContext.forAutoMatching(1L, true); - doThrow(new NullPointerException("Null task status")) - .when(taskStatusBiz).setFaceCutStatus(1L, 0); +// doThrow(new NullPointerException("Null task status")) +// .when(taskStatusBiz).setFaceCutStatus(1L, 0); // When StageResult result = stage.execute(context); diff --git a/src/test/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapterTest.java b/src/test/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapterTest.java index 527c848f..9c7f7ef2 100644 --- a/src/test/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapterTest.java +++ b/src/test/java/com/ycwl/basic/facebody/adapter/AliFaceBodyAdapterTest.java @@ -14,8 +14,8 @@ public class AliFaceBodyAdapterTest { private AliFaceBodyAdapter getAdapter() { AliFaceBodyAdapter adapter = new AliFaceBodyAdapter(); AliFaceBodyConfig config = new AliFaceBodyConfig(); - config.setAccessKeyId("LTAI5tMwrmxVcUEKoH5QzLHx"); - config.setAccessKeySecret("ZCIP8aKx1jwX1wkeYIPQEDZ8fPtN1c"); + config.setAccessKeyId("LTAI5tBr8gLs7oRVzwjmwy5s"); + config.setAccessKeySecret("ZMQcmY97tly9jypfoVqnlnPX2wqvYh"); config.setRegion("cn-shanghai"); adapter.setConfig(config); return adapter;