feat(printer): 新增人脸绑定二维码生成功能

- 添加获取人脸绑定二维码接口,支持生成小程序二维码
- 实现二维码文件流输出及临时文件清理
- 修改人脸识别流程,保存人脸数据并返回faceId
- 调整自动添加照片到打印列表逻辑,返回添加数量
- 更新响应模型,增加二维码URL字段
- 优化人脸匹配逻辑,使用memberRelationRepository查询关联照片
- 修复BCE适配器图片下载地址问题,去除内网地址替换逻辑
This commit is contained in:
2025-12-04 18:21:18 +08:00
parent 15dda645b9
commit 42540e2dc4
5 changed files with 109 additions and 27 deletions

View File

@@ -3,7 +3,13 @@ package com.ycwl.basic.controller.printer;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.annotation.IgnoreToken;
@@ -23,6 +29,7 @@ import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil;
import com.ycwl.basic.utils.WxMpUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -55,8 +62,13 @@ public class PrinterTvController {
private final ScenicRepository scenicRepository;
private final FaceRepository faceRepository;
private final TaskFaceService faceService;
private final FaceService pcFaceService;
private final ScenicService scenicService;
private final SourceMapper sourceMapper;
private final FaceMapper faceMapper;
private final MemberRelationRepository memberRelationRepository;
private final SourceRepository sourceRepository;
private final PrinterService printerService;
/**
* 获取景区列表
@@ -118,6 +130,57 @@ public class PrinterTvController {
}
}
/**
* 获取人脸绑定二维码
* 生成小程序二维码,用于绑定人脸到用户账号
*
* @param faceId 人脸ID
* @param response HTTP响应
*/
@GetMapping("/face/{faceId}/qrcode")
public void getFaceQrcode(@PathVariable("faceId") Long faceId, HttpServletResponse response) throws Exception {
File qrcode = new File("qrcode_face_" + faceId + ".jpg");
try {
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
response.setStatus(404);
return;
}
MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(face.getScenicId());
if (scenicMpConfig == null) {
response.setStatus(500);
return;
}
WxMpUtil.generateUnlimitedWXAQRCode(
scenicMpConfig.getAppId(),
scenicMpConfig.getAppSecret(),
"pages/videoSynthesis/bind_face",
faceId.toString(),
qrcode
);
// 设置响应头
response.setContentType("image/jpeg");
response.setHeader("Content-Disposition", "inline; filename=\"" + qrcode.getName() + "\"");
// 将二维码文件写入响应输出流
try (FileInputStream fis = new FileInputStream(qrcode);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
} finally {
// 删除临时文件
if (qrcode.exists()) {
qrcode.delete();
}
}
}
/**
* 根据人脸样本ID查询图像素材
*
@@ -154,35 +217,44 @@ public class PrinterTvController {
String fileName = UUID.randomUUID() + "." + suffix;
String faceUrl = adapter.uploadFile(file, filePath, fileName);
// 2. 获取景区人脸识别适配器
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
// 2. 保存人脸数据到数据库
Long faceId = SnowFlakeUtil.getLongId();
FaceEntity faceEntity = new FaceEntity();
faceEntity.setId(faceId);
faceEntity.setScenicId(scenicId);
faceEntity.setFaceUrl(faceUrl);
faceEntity.setCreateAt(new Date());
faceEntity.setMemberId(0L); // 打印机大屏端没有用户ID
faceMapper.add(faceEntity);
// 3. 在景区人脸库中搜索(注意:这里使用scenicId作为数据库名,搜索的是景区内的人脸样本)
SearchFaceRespVo searchResult = faceService.searchFace(
faceBodyAdapter,
String.valueOf(scenicId),
faceUrl,
"大屏端人脸识别"
);
pcFaceService.matchFaceId(faceId);
// 4. 查询匹配到的图像素材(type=2)
// 4. 自动添加照片到打印列表
int addedCount = printerService.autoAddPhotosToPreferPrint(faceId);
// 5. 查询匹配到的图像素材(type=2)
List<SourceEntity> sources = new ArrayList<>();
if (searchResult != null && searchResult.getSampleListIds() != null && !searchResult.getSampleListIds().isEmpty()) {
// 遍历匹配到的人脸样本ID,查询对应的图像素材
for (Long sampleId : searchResult.getSampleListIds()) {
SourceEntity source = sourceMapper.getBySampleIdAndType(sampleId, 2);
if (source != null) {
sources.add(source);
}
List<MemberSourceEntity> memberSourceEntities = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
for (MemberSourceEntity memberSourceEntity : memberSourceEntities) {
SourceEntity source = sourceRepository.getSource(memberSourceEntity.getSourceId());
if (source != null) {
sources.add(source);
}
}
// 5. 构造响应
// 6. 构造响应
FaceRecognizeWithSourcesResp resp = new FaceRecognizeWithSourcesResp();
resp.setUrl(faceUrl);
resp.setFaceId(null); // 不保存人脸,所以没有faceId
resp.setFaceId(faceId);
resp.setScenicId(scenicId);
resp.setSources(sources);
// 只有当添加了照片时才返回二维码URL
if (addedCount > 0) {
resp.setQrcodeUrl("https://zhentuai.com/printer/v1/tv/face/" + faceId + "/qrcode");
} else {
resp.setQrcodeUrl(null);
}
return ApiResponse.success(resp);
}

View File

@@ -418,7 +418,7 @@ public class BceFaceBodyAdapter implements IFaceBodyAdapter {
ByteArrayOutputStream baos = null;
try {
// 下载图片
URL url = new URL(imageUrl.replace("oss-cn-shanghai.aliyuncs.com", "oss-cn-shanghai-internal.aliyuncs.com"));
URL url = new URL(imageUrl);
image = ImageIO.read(url);
if (image == null) {
log.error("无法读取图片,URL: {}", imageUrl);

View File

@@ -31,4 +31,9 @@ public class FaceRecognizeWithSourcesResp {
* 匹配到的图像素材列表(type=2)
*/
private List<SourceEntity> sources;
/**
* 二维码URL
*/
private String qrcodeUrl;
}

View File

@@ -61,7 +61,7 @@ public interface PrinterService {
FaceRecognizeResp useSample(Long userId, Long sampleId);
void autoAddPhotosToPreferPrint(Long faceId);
int autoAddPhotosToPreferPrint(Long faceId);
/**
* 查询待审核的打印任务

View File

@@ -1126,15 +1126,16 @@ public class PrinterServiceImpl implements PrinterService {
* 根据景区和设备配置自动添加type=2的照片到用户打印列表
*
* @param faceId 人脸ID
* @return 成功添加的照片数量
*/
@Override
public void autoAddPhotosToPreferPrint(Long faceId) {
public int autoAddPhotosToPreferPrint(Long faceId) {
try {
// 1. 获取人脸信息
FaceEntity face = faceRepository.getFace(faceId);
if (face == null) {
log.warn("人脸不存在,无法自动添加打印: faceId={}", faceId);
return;
return 0;
}
Long scenicId = face.getScenicId();
@@ -1144,21 +1145,21 @@ public class PrinterServiceImpl implements PrinterService {
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig == null) {
log.warn("景区配置不存在,跳过自动添加打印: scenicId={}", scenicId);
return;
return 0;
}
// 3. 检查景区是否启用打印功能
Boolean printEnable = scenicConfig.getBoolean("print_enable");
if (printEnable == null || !printEnable) {
log.debug("景区未启用打印功能,跳过自动添加: scenicId={}", scenicId);
return;
return 0;
}
// 4. 查询该faceId关联的所有type=2的照片
List<SourceEntity> imageSources = sourceMapper.listImageSourcesByFaceId(faceId);
if (imageSources == null || imageSources.isEmpty()) {
log.debug("该人脸没有关联的照片,跳过自动添加: faceId={}", faceId);
return;
return 0;
}
// 5. 按照deviceId分组处理
@@ -1246,15 +1247,19 @@ public class PrinterServiceImpl implements PrinterService {
log.error("等待照片添加任务完成时发生异常: faceId={}", faceId, e);
}
if (totalAdded.get() > 0) {
log.info("自动添加打印完成: faceId={}, 成功添加{}张照片", faceId, totalAdded.get());
int added = totalAdded.get();
if (added > 0) {
log.info("自动添加打印完成: faceId={}, 成功添加{}张照片", faceId, added);
} else {
log.debug("自动添加打印完成: faceId={}, 无符合条件的照片", faceId);
}
return added;
} catch (Exception e) {
// 出现异常则放弃,不影响主流程
log.error("自动添加打印失败,已忽略: faceId={}", faceId, e);
return 0;
}
}
/**